[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]
[taler-merchant-backoffice] branch master updated (b6dd8d0 -> d786453)
From: |
gnunet |
Subject: |
[taler-merchant-backoffice] branch master updated (b6dd8d0 -> d786453) |
Date: |
Thu, 06 Jan 2022 19:36:21 +0100 |
This is an automated email from the git hooks/post-receive script.
ms pushed a change to branch master
in repository merchant-backoffice.
discard b6dd8d0 fix lang changer
discard e96a042 bank i18n: trying without 'useContext'
discard f352920 Solve i18n runtime issues.
discard 45a3f1a bank's i18n
This update removed existing revisions from the reference, leaving the
reference pointing at a previous point in the repository history.
* -- * -- N refs/heads/master (d786453)
\
O -- O -- O (b6dd8d0)
Any revisions marked "omit" are not gone; other references still
refer to them. Any revisions marked "discard" are gone forever.
No new revisions were added by this update.
Summary of changes:
packages/bank/build-bank-translations.sh | 28 --
packages/bank/contrib/po2ts | 42 ---
packages/bank/src/components/app.tsx | 11 +-
packages/bank/src/components/fields/DateInput.tsx | 90 ++++++
packages/bank/src/components/fields/EmailInput.tsx | 57 ++++
packages/bank/src/components/fields/FileInput.tsx | 104 ++++++
packages/bank/src/components/fields/ImageInput.tsx | 93 ++++++
.../bank/src/components/fields/NumberInput.tsx | 56 ++++
packages/bank/src/components/fields/TextInput.tsx | 68 ++++
packages/bank/src/components/menu/LangSelector.tsx | 92 ++++++
.../bank/src/components/menu/NavigationBar.tsx | 79 +++++
packages/bank/src/components/menu/SideBar.tsx | 73 +++++
packages/bank/src/components/menu/index.tsx | 135 ++++++++
packages/bank/src/components/picker/DatePicker.tsx | 356 +++++++++++++++++++++
.../components/picker/DurationPicker.stories.tsx | 45 +--
.../bank/src/components/picker/DurationPicker.tsx | 211 ++++++++++++
packages/bank/src/context/translation.ts | 39 ++-
packages/bank/src/i18n/de.po | 49 ---
packages/bank/src/i18n/en.po | 49 ---
packages/bank/src/i18n/index.tsx | 165 +++++++++-
packages/bank/src/i18n/strings.ts | 73 ++---
.../src/i18n/{bank.pot => taler-anastasis.pot} | 28 +-
packages/bank/src/pages/home/index.tsx | 36 +--
23 files changed, 1674 insertions(+), 305 deletions(-)
delete mode 100755 packages/bank/build-bank-translations.sh
delete mode 100755 packages/bank/contrib/po2ts
create mode 100644 packages/bank/src/components/fields/DateInput.tsx
create mode 100644 packages/bank/src/components/fields/EmailInput.tsx
create mode 100644 packages/bank/src/components/fields/FileInput.tsx
create mode 100644 packages/bank/src/components/fields/ImageInput.tsx
create mode 100644 packages/bank/src/components/fields/NumberInput.tsx
create mode 100644 packages/bank/src/components/fields/TextInput.tsx
create mode 100644 packages/bank/src/components/menu/LangSelector.tsx
create mode 100644 packages/bank/src/components/menu/NavigationBar.tsx
create mode 100644 packages/bank/src/components/menu/SideBar.tsx
create mode 100644 packages/bank/src/components/menu/index.tsx
create mode 100644 packages/bank/src/components/picker/DatePicker.tsx
copy packages/{merchant-backoffice =>
bank}/src/components/picker/DurationPicker.stories.tsx (58%)
create mode 100644 packages/bank/src/components/picker/DurationPicker.tsx
delete mode 100644 packages/bank/src/i18n/de.po
delete mode 100644 packages/bank/src/i18n/en.po
rename packages/bank/src/i18n/{bank.pot => taler-anastasis.pot} (61%)
diff --git a/packages/bank/build-bank-translations.sh
b/packages/bank/build-bank-translations.sh
deleted file mode 100755
index 34176b2..0000000
--- a/packages/bank/build-bank-translations.sh
+++ /dev/null
@@ -1,28 +0,0 @@
-#!/bin/bash
-
-set -eu
-
-function build {
- POTGEN=node_modules/@gnu-taler/pogen/bin/pogen
- PACKAGE_NAME=$1
-
- find \( -name '*.ts' -or -name '*.tsx' \) ! -name '*.d.ts' \
- | xargs node $POTGEN \
- | msguniq \
- | msgmerge src/i18n/poheader - \
- > src/i18n/$PACKAGE_NAME.pot
-
- for pofile in $(ls src/i18n/*.po 2> /dev/null || true); do
- echo merging $pofile;
- msgmerge -o $pofile $pofile src/i18n/$PACKAGE_NAME.pot;
- done;
-
- # generate .ts file containing all translations
- cat src/i18n/strings-prelude > src/i18n/strings.ts
- for pofile in $(ls src/i18n/*.po 2> /dev/null || true); do \
- echo appending $pofile; \
- ./contrib/po2ts $pofile >> src/i18n/strings.ts; \
- done;
-}
-
-build bank
diff --git a/packages/bank/contrib/po2ts b/packages/bank/contrib/po2ts
deleted file mode 100755
index a135da6..0000000
--- a/packages/bank/contrib/po2ts
+++ /dev/null
@@ -1,42 +0,0 @@
-#!/usr/bin/env node
-/*
- This file is part of GNU Taler
- (C) 2020 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/>
- */
-
-/**
- * Convert a <lang>.po file into a JavaScript / TypeScript expression.
- */
-
-const po2json = require("po2json");
-
-const filename = process.argv[2];
-
-if (!filename) {
- console.error("error: missing filename");
- process.exit(1);
-}
-
-const m = filename.match(/([a-zA-Z0-9-_]+).po/);
-
-if (!m) {
- console.error("error: unexpected filename (expected <lang>.po)");
- process.exit(1);
-}
-
-const lang = m[1];
-const pojson = po2json.parseFileSync(filename, { format: "jed1.x", fuzzy: true
});
-const s =
- "strings['" + lang + "'] = " + JSON.stringify(pojson, null, " ") + ";\n";
-console.log(s);
diff --git a/packages/bank/src/components/app.tsx
b/packages/bank/src/components/app.tsx
index 17325c0..5739f3a 100644
--- a/packages/bank/src/components/app.tsx
+++ b/packages/bank/src/components/app.tsx
@@ -1,9 +1,14 @@
import { FunctionalComponent, h } from "preact";
+import { TranslationProvider } from "../context/translation";
import { BankHome } from "../pages/home/index";
+import { Menu } from "./menu";
-const AppI18N: FunctionalComponent = () => {
- return (<BankHome />);
+const App: FunctionalComponent = () => {
+ return (
+ <TranslationProvider>
+ <BankHome />
+ </TranslationProvider>
+ );
};
-const App = AppI18N;
export default App;
diff --git a/packages/bank/src/components/fields/DateInput.tsx
b/packages/bank/src/components/fields/DateInput.tsx
new file mode 100644
index 0000000..18ef899
--- /dev/null
+++ b/packages/bank/src/components/fields/DateInput.tsx
@@ -0,0 +1,90 @@
+import { format, 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>;
+ onConfirm?: () => void;
+ 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}
+ onKeyPress={(e) => {
+ if (e.key === 'Enter' && props.onConfirm) {
+ props.onConfirm()
+ }
+ }}
+ 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/bank/src/components/fields/EmailInput.tsx
b/packages/bank/src/components/fields/EmailInput.tsx
new file mode 100644
index 0000000..4c35c06
--- /dev/null
+++ b/packages/bank/src/components/fields/EmailInput.tsx
@@ -0,0 +1,57 @@
+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;
+ onConfirm?: () => void;
+ 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"}
+ onKeyPress={(e) => {
+ if (e.key === 'Enter' && props.onConfirm) {
+ props.onConfirm()
+ }
+ }}
+ 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/bank/src/components/fields/FileInput.tsx
b/packages/bank/src/components/fields/FileInput.tsx
new file mode 100644
index 0000000..adf51af
--- /dev/null
+++ b/packages/bank/src/components/fields/FileInput.tsx
@@ -0,0 +1,104 @@
+/*
+ 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";
+
+const MAX_IMAGE_UPLOAD_SIZE = 1024 * 1024;
+
+export interface FileTypeContent {
+ content: string;
+ type: string;
+ name: string;
+}
+
+export interface FileInputProps {
+ label: string;
+ grabFocus?: boolean;
+ disabled?: boolean;
+ error?: string;
+ placeholder?: string;
+ tooltip?: string;
+ onChange: (v: FileTypeContent | undefined) => void;
+}
+
+export function FileInput(props: FileInputProps): VNode {
+ const inputRef = useRef<HTMLInputElement>(null);
+ useLayoutEffect(() => {
+ if (props.grabFocus) {
+ inputRef.current?.focus();
+ }
+ }, [props.grabFocus]);
+
+ const fileInputRef = useRef<HTMLInputElement>(null);
+ const [sizeError, setSizeError] = useState(false);
+ return (
+ <div class="field">
+ <label class="label">
+ <a class="button" onClick={(e) => fileInputRef.current?.click()}>
+ <div class="icon is-small ">
+ <i class="mdi mdi-folder" />
+ </div>
+ <span>
+ {props.label}
+ </span>
+ </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={fileInputRef}
+ style={{ display: "none" }}
+ type="file"
+ // name={String(name)}
+ onChange={(e) => {
+ const f: FileList | null = e.currentTarget.files;
+ if (!f || f.length != 1) {
+ return props.onChange(undefined);
+ }
+ console.log(f)
+ if (f[0].size > MAX_IMAGE_UPLOAD_SIZE) {
+ setSizeError(true);
+ return props.onChange(undefined);
+ }
+ setSizeError(false);
+ return f[0].arrayBuffer().then((b) => {
+ const b64 = btoa(
+ new Uint8Array(b).reduce(
+ (data, byte) => data + String.fromCharCode(byte),
+ "",
+ ),
+ );
+ return props.onChange({content:
`data:${f[0].type};base64,${b64}`, name: f[0].name, type: f[0].type});
+ });
+ }}
+ />
+ {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/bank/src/components/fields/ImageInput.tsx
b/packages/bank/src/components/fields/ImageInput.tsx
new file mode 100644
index 0000000..3f8cc58
--- /dev/null
+++ b/packages/bank/src/components/fields/ImageInput.tsx
@@ -0,0 +1,93 @@
+/*
+ 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/bank/src/components/fields/NumberInput.tsx
b/packages/bank/src/components/fields/NumberInput.tsx
new file mode 100644
index 0000000..4856131
--- /dev/null
+++ b/packages/bank/src/components/fields/NumberInput.tsx
@@ -0,0 +1,56 @@
+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;
+ onConfirm?: () => void;
+ bind: [string, (x: string) => void];
+}
+
+export function PhoneNumberInput(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="tel"
+ placeholder={props.placeholder}
+ class={showError ? "input is-danger" : "input"}
+ onKeyPress={(e) => {
+ if (e.key === 'Enter' && props.onConfirm) {
+ props.onConfirm()
+ }
+ }}
+ 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/bank/src/components/fields/TextInput.tsx
b/packages/bank/src/components/fields/TextInput.tsx
new file mode 100644
index 0000000..55643b4
--- /dev/null
+++ b/packages/bank/src/components/fields/TextInput.tsx
@@ -0,0 +1,68 @@
+import { h, VNode } from "preact";
+import { useLayoutEffect, useRef, useState } from "preact/hooks";
+
+export interface TextInputProps {
+ inputType?: "text" | "number" | "multiline" | "password";
+ label: string;
+ grabFocus?: boolean;
+ disabled?: boolean;
+ error?: string;
+ placeholder?: string;
+ tooltip?: string;
+ onConfirm?: () => void;
+ bind: [string, (x: string) => void];
+}
+
+const TextInputType = function ({ inputType, grabFocus, ...rest }: any): VNode
{
+ const inputRef = useRef<HTMLInputElement>(null);
+ useLayoutEffect(() => {
+ if (grabFocus) {
+ inputRef.current?.focus();
+ }
+ }, [grabFocus]);
+
+ return inputType === "multiline" ? (
+ <textarea {...rest} rows={5} ref={inputRef} style={{ height: "unset" }} />
+ ) : (
+ <input {...rest} type={inputType} ref={inputRef} />
+ );
+};
+
+export function TextInput(props: TextInputProps): VNode {
+ 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">
+ <TextInputType
+ inputType={props.inputType}
+ value={value}
+ grabFocus={props.grabFocus}
+ disabled={props.disabled}
+ placeholder={props.placeholder}
+ class={showError ? "input is-danger" : "input"}
+ onKeyPress={(e: any) => {
+ if (e.key === "Enter" && props.onConfirm) {
+ props.onConfirm();
+ }
+ }}
+ onInput={(e: any) => {
+ setDirty(true);
+ props.bind[1]((e.target as HTMLInputElement).value);
+ }}
+ style={{ display: "block" }}
+ />
+ </div>
+ {showError && <p class="help is-danger">{props.error}</p>}
+ </div>
+ );
+}
diff --git a/packages/bank/src/components/menu/LangSelector.tsx
b/packages/bank/src/components/menu/LangSelector.tsx
new file mode 100644
index 0000000..fa22a29
--- /dev/null
+++ b/packages/bank/src/components/menu/LangSelector.tsx
@@ -0,0 +1,92 @@
+/*
+ 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 langIcon from "../../assets/icons/languageicon.svg";
+import { useTranslationContext } from "../../context/translation";
+import { strings as messages } from "../../i18n/strings";
+
+type LangsNames = {
+ [P in keyof typeof messages]: string;
+};
+
+const names: LangsNames = {
+ es: "Español [es]",
+ en: "English [en]",
+ fr: "Français [fr]",
+ de: "Deutsch [de]",
+ sv: "Svenska [sv]",
+ it: "Italiano [it]",
+};
+
+function getLangName(s: keyof LangsNames | string): string {
+ if (names[s]) return names[s];
+ return String(s);
+}
+
+export function LangSelector(): VNode {
+ const [updatingLang, setUpdatingLang] = useState(false);
+ const { lang, changeLanguage } = useTranslationContext();
+
+ return (
+ <div class="dropdown is-active ">
+ <div class="dropdown-trigger">
+ <button
+ class="button has-tooltip-left"
+ data-tooltip="change language selection"
+ aria-haspopup="true"
+ aria-controls="dropdown-menu"
+ onClick={() => setUpdatingLang(!updatingLang)}
+ >
+ <div class="icon is-small is-left">
+ <img src={langIcon} />
+ </div>
+ <span>{getLangName(lang)}</span>
+ <div class="icon is-right">
+ <i class="mdi mdi-chevron-down" />
+ </div>
+ </button>
+ </div>
+ {updatingLang && (
+ <div class="dropdown-menu" id="dropdown-menu" role="menu">
+ <div class="dropdown-content">
+ {Object.keys(messages)
+ .filter((l) => l !== lang)
+ .map((l) => (
+ <a
+ key={l}
+ class="dropdown-item"
+ value={l}
+ onClick={() => {
+ changeLanguage(l);
+ setUpdatingLang(false);
+ }}
+ >
+ {getLangName(l)}
+ </a>
+ ))}
+ </div>
+ </div>
+ )}
+ </div>
+ );
+}
diff --git a/packages/bank/src/components/menu/NavigationBar.tsx
b/packages/bank/src/components/menu/NavigationBar.tsx
new file mode 100644
index 0000000..b7876a4
--- /dev/null
+++ b/packages/bank/src/components/menu/NavigationBar.tsx
@@ -0,0 +1,79 @@
+/*
+ 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 logo from "../../assets/logo.jpeg";
+import { LangSelector } from "./LangSelector";
+
+interface Props {
+ onMobileMenu: () => void;
+ title: string;
+}
+
+export function NavigationBar({ onMobileMenu, title }: Props): VNode {
+ return (
+ <nav
+ class="navbar is-fixed-top"
+ role="navigation"
+ aria-label="main navigation"
+ >
+ <div class="navbar-brand">
+ <span class="navbar-item" style={{ fontSize: 24, fontWeight: 900 }}>
+ {title}
+ </span>
+ {/* <a
+ href="mailto:contact@anastasis.lu"
+ style={{ alignSelf: "center", padding: "0.5em" }}
+ >
+ Contact us
+ </a>
+ <a
+ href="https://bugs.anastasis.li/"
+ style={{ alignSelf: "center", padding: "0.5em" }}
+ >
+ Report a bug
+ </a> */}
+ {/* <a
+ role="button"
+ class="navbar-burger"
+ aria-label="menu"
+ aria-expanded="false"
+ onClick={(e) => {
+ onMobileMenu();
+ e.stopPropagation();
+ }}
+ >
+ <span aria-hidden="true" />
+ <span aria-hidden="true" />
+ <span aria-hidden="true" />
+ </a> */}
+ </div>
+
+ <div class="navbar-menu ">
+ <div class="navbar-end">
+ <div class="navbar-item" style={{ paddingTop: 4, paddingBottom: 4 }}>
+ {/* <LangSelector /> */}
+ </div>
+ </div>
+ </div>
+ </nav>
+ );
+}
diff --git a/packages/bank/src/components/menu/SideBar.tsx
b/packages/bank/src/components/menu/SideBar.tsx
new file mode 100644
index 0000000..d7833df
--- /dev/null
+++ b/packages/bank/src/components/menu/SideBar.tsx
@@ -0,0 +1,73 @@
+/*
+ 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 { Translate } from "../../i18n";
+
+interface Props {
+ mobile?: boolean;
+}
+
+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" } };
+
+ return (
+ <aside class="aside is-placed-left is-expanded">
+ <div class="aside-tools">
+ <div class="aside-tools-label">
+ <div>
+ <b>euFin bank</b>
+ </div>
+ <div
+ class="is-size-7 has-text-right"
+ style={{ lineHeight: 0, marginTop: -10 }}
+ >
+ Version {process.env.__VERSION__} ({config.version})
+ </div>
+ </div>
+ </div>
+ <div class="menu is-menu-main">
+ <p class="menu-label">
+ <Translate>Bank menu</Translate>
+ </p>
+ <ul class="menu-list">
+ <li>
+ <div class="ml-4">
+ <span class="menu-item-label">
+ <Translate>Select option1</Translate>
+ </span>
+ </div>
+ </li>
+ <li>
+ <div class="ml-4">
+ <span class="menu-item-label">
+ <Translate>Select option2</Translate>
+ </span>
+ </div>
+ </li>
+ </ul>
+ </div>
+ </aside>
+ );
+}
diff --git a/packages/bank/src/components/menu/index.tsx
b/packages/bank/src/components/menu/index.tsx
new file mode 100644
index 0000000..99d0f76
--- /dev/null
+++ b/packages/bank/src/components/menu/index.tsx
@@ -0,0 +1,135 @@
+/*
+ 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 { ComponentChildren, Fragment, h, VNode } from "preact";
+import Match from "preact-router/match";
+import { useEffect, useState } from "preact/hooks";
+import { NavigationBar } from "./NavigationBar";
+import { Sidebar } from "./SideBar";
+
+interface MenuProps {
+ title: string;
+}
+
+function WithTitle({
+ title,
+ children,
+}: {
+ title: string;
+ children: ComponentChildren;
+}): VNode {
+ useEffect(() => {
+ document.title = `${title}`;
+ }, [title]);
+ return <Fragment>{children}</Fragment>;
+}
+
+export function Menu({ title }: MenuProps): VNode {
+ const [mobileOpen, setMobileOpen] = useState(false);
+
+ return (
+ <Match>
+ {({ path }: { path: string }) => {
+ const titleWithSubtitle = title; // title ? title : (!admin ?
getInstanceTitle(path, instance) : getAdminTitle(path, instance))
+ return (
+ <WithTitle title={titleWithSubtitle}>
+ <div
+ class={mobileOpen ? "has-aside-mobile-expanded" : ""}
+ onClick={() => setMobileOpen(false)}
+ >
+ <NavigationBar
+ onMobileMenu={() => setMobileOpen(!mobileOpen)}
+ title={titleWithSubtitle}
+ />
+
+ <Sidebar mobile={mobileOpen} />
+ </div>
+ </WithTitle>
+ );
+ }}
+ </Match>
+ );
+}
+
+interface NotYetReadyAppMenuProps {
+ title: string;
+ onLogout?: () => void;
+}
+
+interface NotifProps {
+ notification?: Notification;
+}
+export function NotificationCard({
+ notification: n,
+}: NotifProps): VNode | null {
+ if (!n) return null;
+ return (
+ <div class="notification">
+ <div class="columns is-vcentered">
+ <div class="column is-12">
+ <article
+ class={
+ n.type === "ERROR"
+ ? "message is-danger"
+ : n.type === "WARN"
+ ? "message is-warning"
+ : "message is-info"
+ }
+ >
+ <div class="message-header">
+ <p>{n.message}</p>
+ </div>
+ {n.description && <div class="message-body">{n.description}</div>}
+ </article>
+ </div>
+ </div>
+ </div>
+ );
+}
+
+export function NotYetReadyAppMenu({
+ onLogout,
+ title,
+}: NotYetReadyAppMenuProps): VNode {
+ const [mobileOpen, setMobileOpen] = useState(false);
+
+ useEffect(() => {
+ document.title = `Taler Backoffice: ${title}`;
+ }, [title]);
+
+ return (
+ <div
+ class="has-aside-mobile-expanded"
+ // class={mobileOpen ? "has-aside-mobile-expanded" : ""}
+ onClick={() => setMobileOpen(false)}
+ >
+ <NavigationBar
+ onMobileMenu={() => setMobileOpen(!mobileOpen)}
+ title={title}
+ />
+ {onLogout && <Sidebar mobile={mobileOpen} />}
+ </div>
+ );
+}
+
+export interface Notification {
+ message: string;
+ description?: string | VNode;
+ type: MessageType;
+}
+
+export type ValueOrFunction<T> = T | ((p: T) => T);
+export type MessageType = "INFO" | "WARN" | "ERROR" | "SUCCESS";
diff --git a/packages/bank/src/components/picker/DatePicker.tsx
b/packages/bank/src/components/picker/DatePicker.tsx
new file mode 100644
index 0000000..d689db3
--- /dev/null
+++ b/packages/bank/src/components/picker/DatePicker.tsx
@@ -0,0 +1,356 @@
+/*
+ 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/merchant-backoffice/src/components/picker/DurationPicker.stories.tsx
b/packages/bank/src/components/picker/DurationPicker.stories.tsx
similarity index 58%
copy from
packages/merchant-backoffice/src/components/picker/DurationPicker.stories.tsx
copy to packages/bank/src/components/picker/DurationPicker.stories.tsx
index 275c80f..7f96cc1 100644
---
a/packages/merchant-backoffice/src/components/picker/DurationPicker.stories.tsx
+++ b/packages/bank/src/components/picker/DurationPicker.stories.tsx
@@ -15,36 +15,41 @@
*/
/**
-*
-* @author Sebastian Javier Marchano (sebasjm)
-*/
-
-import { h, FunctionalComponent } from 'preact';
-import { useState } from 'preact/hooks';
-import { DurationPicker as TestedComponent } from './DurationPicker';
+ *
+ * @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',
+ title: "Components/Picker/Duration",
component: TestedComponent,
argTypes: {
- onCreate: { action: 'onCreate' },
- goBack: { action: 'goBack' },
- }
+ 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
+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
+ 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 />
-}
+ const [v, s] = useState<number>(1000000);
+ return <TestedComponent value={v} onChange={s} days minutes hours seconds />;
+};
diff --git a/packages/bank/src/components/picker/DurationPicker.tsx
b/packages/bank/src/components/picker/DurationPicker.tsx
new file mode 100644
index 0000000..8a1faf4
--- /dev/null
+++ b/packages/bank/src/components/picker/DurationPicker.tsx
@@ -0,0 +1,211 @@
+/*
+ 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}`;
+}
diff --git a/packages/bank/src/context/translation.ts
b/packages/bank/src/context/translation.ts
index 4953cfa..a47864d 100644
--- a/packages/bank/src/context/translation.ts
+++ b/packages/bank/src/context/translation.ts
@@ -20,12 +20,47 @@
*/
import { createContext, h, VNode } from "preact";
-import { useState, useContext, useEffect } from "preact/hooks";
+import { useContext, useEffect } from "preact/hooks";
import { useLang } from "../hooks";
import * as jedLib from "jed";
import { strings } from "../i18n/strings";
-export interface TranslationStateType {
+interface Type {
lang: string;
handler: any;
+ changeLanguage: (l: string) => void;
}
+const initial = {
+ lang: "en",
+ handler: null,
+ changeLanguage: () => {
+ // do not change anything
+ },
+};
+const Context = createContext<Type>(initial);
+
+interface Props {
+ initial?: string;
+ children: any;
+ forceLang?: string;
+}
+
+export const TranslationProvider = ({
+ initial,
+ children,
+ forceLang,
+}: Props): VNode => {
+ const [lang, changeLanguage] = useLang(initial);
+ useEffect(() => {
+ if (forceLang) {
+ changeLanguage(forceLang);
+ }
+ });
+ const handler = new jedLib.Jed(strings[lang] || strings["en"]);
+ return h(Context.Provider, {
+ value: { lang, handler, changeLanguage },
+ children,
+ });
+};
+
+export const useTranslationContext = (): Type => useContext(Context);
diff --git a/packages/bank/src/i18n/de.po b/packages/bank/src/i18n/de.po
deleted file mode 100644
index 4c30069..0000000
--- a/packages/bank/src/i18n/de.po
+++ /dev/null
@@ -1,49 +0,0 @@
-#:
/home/job/merchant-backoffice/packages/bank/src/components/picker/DurationPicker.tsx:55
-#, c-format
-msgid "days"
-msgstr ""
-
-#:
/home/job/merchant-backoffice/packages/bank/src/components/picker/DurationPicker.tsx:65
-#, c-format
-msgid "hours"
-msgstr ""
-
-#:
/home/job/merchant-backoffice/packages/bank/src/components/picker/DurationPicker.tsx:76
-#, c-format
-msgid "minutes"
-msgstr ""
-
-#:
/home/job/merchant-backoffice/packages/bank/src/components/picker/DurationPicker.tsx:87
-#, c-format
-msgid "seconds"
-msgstr ""
-
-#: /home/job/merchant-backoffice/packages/bank/src/pages/home/index.tsx:553
-#, c-format
-msgid "Page has a problem:"
-msgstr ""
-
-# 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/>
-#
-msgid ""
-msgstr ""
-"Project-Id-Version: Taler Wallet\n"
-"Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2016-11-23 00:00+0100\n"
-"PO-Revision-Date: 2022-01-03 10:27+0100\n"
-"Last-Translator: <translations@taler.net>\n"
-"Language-Team: German\n"
-"Language: de\n"
-"MIME-Version: 1.0\n"
-"Content-Type: text/plain; charset=UTF-8\n"
-"Content-Transfer-Encoding: 8bit\n"
-"Plural-Forms: nplurals=2; plural=(n != 1);\n"
diff --git a/packages/bank/src/i18n/en.po b/packages/bank/src/i18n/en.po
deleted file mode 100644
index 39903e8..0000000
--- a/packages/bank/src/i18n/en.po
+++ /dev/null
@@ -1,49 +0,0 @@
-#:
/home/job/merchant-backoffice/packages/bank/src/components/picker/DurationPicker.tsx:55
-#, c-format
-msgid "days"
-msgstr "days"
-
-#:
/home/job/merchant-backoffice/packages/bank/src/components/picker/DurationPicker.tsx:65
-#, c-format
-msgid "hours"
-msgstr "hours"
-
-#:
/home/job/merchant-backoffice/packages/bank/src/components/picker/DurationPicker.tsx:76
-#, c-format
-msgid "minutes"
-msgstr "minutes"
-
-#:
/home/job/merchant-backoffice/packages/bank/src/components/picker/DurationPicker.tsx:87
-#, c-format
-msgid "seconds"
-msgstr "seconds"
-
-#: /home/job/merchant-backoffice/packages/bank/src/pages/home/index.tsx:553
-#, c-format
-msgid "Page has a problem:"
-msgstr "Page has a problem:"
-
-# 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/>
-#
-msgid ""
-msgstr ""
-"Project-Id-Version: Taler Wallet\n"
-"Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2016-11-23 00:00+0100\n"
-"PO-Revision-Date: 2022-01-05 13:40+0100\n"
-"Last-Translator: <translations@taler.net>\n"
-"Language-Team: English\n"
-"Language: en\n"
-"MIME-Version: 1.0\n"
-"Content-Type: text/plain; charset=UTF-8\n"
-"Content-Transfer-Encoding: 8bit\n"
-"Plural-Forms: nplurals=2; plural=(n != 1);\n"
diff --git a/packages/bank/src/i18n/index.tsx b/packages/bank/src/i18n/index.tsx
index c3503fa..6e2c4e7 100644
--- a/packages/bank/src/i18n/index.tsx
+++ b/packages/bank/src/i18n/index.tsx
@@ -18,7 +18,16 @@
* Translation helpers for React components and template literals.
*/
-export function useTranslator(jed: any) {
+/**
+ * Imports
+ */
+import { ComponentChild, ComponentChildren, h, Fragment, VNode } from "preact";
+
+import { useTranslationContext } from "../context/translation";
+
+export function useTranslator() {
+ const ctx = useTranslationContext();
+ const jed = ctx.handler;
return function str(
stringSeq: TemplateStringsArray,
...values: any[]
@@ -46,3 +55,157 @@ function toI18nString(stringSeq: ReadonlyArray<string>):
string {
}
return s;
}
+
+interface TranslateSwitchProps {
+ target: number;
+ children: ComponentChildren;
+}
+
+function stringifyChildren(children: ComponentChildren): string {
+ let n = 1;
+ const ss = (children instanceof Array ? children : [children]).map((c) => {
+ if (typeof c === "string") {
+ return c;
+ }
+ return `%${n++}$s`;
+ });
+ const s = ss.join("").replace(/ +/g, " ").trim();
+ return s;
+}
+
+interface TranslateProps {
+ children: ComponentChildren;
+ /**
+ * Component that the translated element should be wrapped in.
+ * Defaults to "div".
+ */
+ wrap?: any;
+
+ /**
+ * Props to give to the wrapped component.
+ */
+ wrapProps?: any;
+}
+
+function getTranslatedChildren(
+ translation: string,
+ children: ComponentChildren,
+): ComponentChild[] {
+ const tr = translation.split(/%(\d+)\$s/);
+ const childArray = children instanceof Array ? children : [children];
+ // Merge consecutive string children.
+ const placeholderChildren = Array<ComponentChild>();
+ for (let i = 0; i < childArray.length; i++) {
+ const x = childArray[i];
+ if (x === undefined) {
+ continue;
+ } else if (typeof x === "string") {
+ continue;
+ } else {
+ placeholderChildren.push(x);
+ }
+ }
+ const result = Array<ComponentChild>();
+ for (let i = 0; i < tr.length; i++) {
+ if (i % 2 == 0) {
+ // Text
+ result.push(tr[i]);
+ } else {
+ const childIdx = Number.parseInt(tr[i], 10) - 1;
+ result.push(placeholderChildren[childIdx]);
+ }
+ }
+ return result;
+}
+
+/**
+ * Translate text node children of this component.
+ * If a child component might produce a text node, it must be wrapped
+ * in a another non-text element.
+ *
+ * Example:
+ * ```
+ * <Translate>
+ * Hello. Your score is <span><PlayerScore player={player} /></span>
+ * </Translate>
+ * ```
+ */
+export function Translate({ children }: TranslateProps): VNode {
+ const s = stringifyChildren(children);
+ const ctx = useTranslationContext();
+ const translation: string = ctx.handler.ngettext(s, s, 1);
+ const result = getTranslatedChildren(translation, children);
+ return <Fragment>{result}</Fragment>;
+}
+
+/**
+ * Switch translation based on singular or plural based on the target prop.
+ * Should only contain TranslateSingular and TransplatePlural as children.
+ *
+ * Example:
+ * ```
+ * <TranslateSwitch target={n}>
+ * <TranslateSingular>I have {n} apple.</TranslateSingular>
+ * <TranslatePlural>I have {n} apples.</TranslatePlural>
+ * </TranslateSwitch>
+ * ```
+ */
+export function TranslateSwitch({ children, target }: TranslateSwitchProps) {
+ let singular: VNode<TranslationPluralProps> | undefined;
+ let plural: VNode<TranslationPluralProps> | undefined;
+ // const children = this.props.children;
+ if (children) {
+ (children instanceof Array ? children : [children]).forEach(
+ (child: any) => {
+ if (child.type === TranslatePlural) {
+ plural = child;
+ }
+ if (child.type === TranslateSingular) {
+ singular = child;
+ }
+ },
+ );
+ }
+ if (!singular || !plural) {
+ console.error("translation not found");
+ return h("span", {}, ["translation not found"]);
+ }
+ singular.props.target = target;
+ plural.props.target = target;
+ // We're looking up the translation based on the
+ // singular, even if we must use the plural form.
+ return singular;
+}
+
+interface TranslationPluralProps {
+ children: ComponentChildren;
+ target: number;
+}
+
+/**
+ * See [[TranslateSwitch]].
+ */
+export function TranslatePlural({
+ children,
+ target,
+}: TranslationPluralProps): VNode {
+ const s = stringifyChildren(children);
+ const ctx = useTranslationContext();
+ const translation = ctx.handler.ngettext(s, s, 1);
+ const result = getTranslatedChildren(translation, children);
+ return <Fragment>{result}</Fragment>;
+}
+
+/**
+ * See [[TranslateSwitch]].
+ */
+export function TranslateSingular({
+ children,
+ target,
+}: TranslationPluralProps): VNode {
+ const s = stringifyChildren(children);
+ const ctx = useTranslationContext();
+ const translation = ctx.handler.ngettext(s, s, target);
+ const result = getTranslatedChildren(translation, children);
+ return <Fragment>{result}</Fragment>;
+}
diff --git a/packages/bank/src/i18n/strings.ts
b/packages/bank/src/i18n/strings.ts
index fb88fe7..d12e63e 100644
--- a/packages/bank/src/i18n/strings.ts
+++ b/packages/bank/src/i18n/strings.ts
@@ -15,61 +15,30 @@
*/
/*eslint quote-props: ["error", "consistent"]*/
-export const strings: {[s: string]: any} = {};
+export const strings: { [s: string]: any } = {};
-strings['de'] = {
- "domain": "messages",
- "locale_data": {
- "messages": {
- "days": [
- ""
- ],
- "hours": [
- ""
- ],
- "minutes": [
- ""
- ],
- "seconds": [
- ""
- ],
- "Page has a problem:": [
- "Es gibt ein Problem:"
- ],
+strings["de"] = {
+ domain: "messages",
+ locale_data: {
+ messages: {
"": {
- "domain": "messages",
- "plural_forms": "nplurals=2; plural=(n != 1);",
- "lang": "de"
- }
- }
- }
+ domain: "messages",
+ plural_forms: "nplurals=2; plural=(n != 1);",
+ lang: "",
+ },
+ },
+ },
};
-strings['en'] = {
- "domain": "messages",
- "locale_data": {
- "messages": {
- "days": [
- "days"
- ],
- "hours": [
- "hours"
- ],
- "minutes": [
- "minutes"
- ],
- "seconds": [
- "seconds"
- ],
- "Page has a problem:": [
- "Page has a problem:"
- ],
+strings["en"] = {
+ domain: "messages",
+ locale_data: {
+ messages: {
"": {
- "domain": "messages",
- "plural_forms": "nplurals=2; plural=(n != 1);",
- "lang": "en"
- }
- }
- }
+ domain: "messages",
+ plural_forms: "nplurals=2; plural=(n != 1);",
+ lang: "",
+ },
+ },
+ },
};
-
diff --git a/packages/bank/src/i18n/bank.pot
b/packages/bank/src/i18n/taler-anastasis.pot
similarity index 61%
rename from packages/bank/src/i18n/bank.pot
rename to packages/bank/src/i18n/taler-anastasis.pot
index 184d908..7cdbc04 100644
--- a/packages/bank/src/i18n/bank.pot
+++ b/packages/bank/src/i18n/taler-anastasis.pot
@@ -1,28 +1,3 @@
-#:
/home/job/merchant-backoffice/packages/bank/src/components/picker/DurationPicker.tsx:55
-#, c-format
-msgid "days"
-msgstr ""
-
-#:
/home/job/merchant-backoffice/packages/bank/src/components/picker/DurationPicker.tsx:65
-#, c-format
-msgid "hours"
-msgstr ""
-
-#:
/home/job/merchant-backoffice/packages/bank/src/components/picker/DurationPicker.tsx:76
-#, c-format
-msgid "minutes"
-msgstr ""
-
-#:
/home/job/merchant-backoffice/packages/bank/src/components/picker/DurationPicker.tsx:87
-#, c-format
-msgid "seconds"
-msgstr ""
-
-#: /home/job/merchant-backoffice/packages/bank/src/pages/home/index.tsx:553
-#, c-format
-msgid "Page has a problem:"
-msgstr ""
-
# 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
@@ -37,7 +12,7 @@ msgstr ""
#, fuzzy
msgid ""
msgstr ""
-"Project-Id-Version: Taler Wallet\n"
+"Project-Id-Version: Taler Bank\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2016-11-23 00:00+0100\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
@@ -48,3 +23,4 @@ msgstr ""
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
+
diff --git a/packages/bank/src/pages/home/index.tsx
b/packages/bank/src/pages/home/index.tsx
index f79e2b8..beb19aa 100644
--- a/packages/bank/src/pages/home/index.tsx
+++ b/packages/bank/src/pages/home/index.tsx
@@ -4,18 +4,11 @@ import { useState, useEffect, StateUpdater } from
"preact/hooks";
import { Buffer } from "buffer";
import { useTranslator } from "../../i18n";
import { QR } from "../../components/QR";
-import * as jedLib from "jed";
-import { strings } from "../../i18n/strings";
-/*********************************************
+/**********************************************
* Type definitions for states and API calls. *
*********************************************/
-interface LangStateType {
- lang: string;
- handler: any;
-}
-
/**
* Has the information to reach and
* authenticate at the bank's backend.
@@ -63,25 +56,6 @@ interface AccountStateType {
* Helpers. *
***********/
-
-/**
- * Trigger language change in the state. Note: there
- * is _no_ check on whether the new language exists.
- */
-function changeLang(
- newLang: string,
- langState: LangStateType,
- langStateSetter: StateUpdater<LangStateType>
-) {
- if (newLang == langState.lang) return;
-
- let newLangState = {
- lang: newLang,
- handler: new jedLib.Jed(strings[newLang])
- }
- langStateSetter(newLangState);
-}
-
/**
* Craft headers with Authorization and Content-Type.
*/
@@ -568,13 +542,9 @@ export function BankHome(): VNode {
var [backendState, backendStateSetter] = useBackendState();
var [pageState, pageStateSetter] = usePageState();
var [accountState, accountStateSetter] = useAccountState();
- var [langState, langStateSetter] = useState<LangStateType>({
- lang: "en",
- handler: new jedLib.Jed(strings["en"]),
- });
- let i18n = useTranslator(langState.handler);
+
if (pageState.hasError) {
- return <p>{i18n`Page has a problem:`} {pageState.error}</p>;
+ return <p>Page has a problem: {pageState.error}</p>;
}
/**
--
To stop receiving notification emails like this one, please contact
gnunet@gnunet.org.
[Prev in Thread] |
Current Thread |
[Next in Thread] |
- [taler-merchant-backoffice] branch master updated (b6dd8d0 -> d786453),
gnunet <=