gnunet-svn
[Top][All Lists]
Advanced

[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.



reply via email to

[Prev in Thread] Current Thread [Next in Thread]