diff options
Diffstat (limited to 'packages')
19 files changed, 477 insertions, 474 deletions
diff --git a/packages/demobank-ui/build.mjs b/packages/demobank-ui/build.mjs index 63ddc1f25..c93b4eb67 100755 --- a/packages/demobank-ui/build.mjs +++ b/packages/demobank-ui/build.mjs @@ -18,9 +18,9 @@ import esbuild from "esbuild"; import path from "path"; import fs from "fs"; -import crypto from "crypto"; -import { sassPlugin } from "esbuild-sass-plugin"; +import sass from "sass"; +// eslint-disable-next-line no-undef const BASE = process.cwd(); const preact = path.join( @@ -44,14 +44,16 @@ const preactCompatPlugin = { }, }; -const entryPoints = ["src/index.tsx"]; +const entryPoints = ["src/index.tsx", "src/stories.tsx"]; let GIT_ROOT = BASE; while (!fs.existsSync(path.join(GIT_ROOT, ".git")) && GIT_ROOT !== "/") { GIT_ROOT = path.join(GIT_ROOT, "../"); } if (GIT_ROOT === "/") { + // eslint-disable-next-line no-undef console.log("not found"); + // eslint-disable-next-line no-undef process.exit(1); } const GIT_HASH = GIT_ROOT === "/" ? undefined : git_hash(); @@ -86,6 +88,26 @@ function copyFilesPlugin(options) { }; } +const DEFAULT_SASS_FILTER = /\.(s[ac]ss|css)$/ + +const buildSassPlugin = { + name: "custom-build-sass", + setup(build) { + + build.onLoad({ filter: DEFAULT_SASS_FILTER }, ({ path: file }) => { + const resolveDir = path.dirname(file) + const { css: contents } = sass.compile(file, { loadPaths: ["./"] }) + + return { + resolveDir, + loader: 'css', + contents + } + }); + + }, +}; + export const buildConfig = { entryPoints: [...entryPoints], bundle: true, @@ -95,6 +117,10 @@ export const buildConfig = { ".svg": "file", ".png": "dataurl", ".jpeg": "dataurl", + '.ttf': 'file', + '.woff': 'file', + '.woff2': 'file', + '.eot': 'file', }, target: ["es6"], format: "esm", @@ -108,17 +134,14 @@ export const buildConfig = { }, plugins: [ preactCompatPlugin, - sassPlugin(), copyFilesPlugin([ { - src: "static/index.html", - dest: "dist/index.html", + src: "./src/index.html", + dest: "./dist/index.html", }, ]), + buildSassPlugin ], }; -esbuild.build(buildConfig).catch((e) => { - console.log(e); - process.exit(1); -}); +await esbuild.build(buildConfig) diff --git a/packages/demobank-ui/dev.mjs b/packages/demobank-ui/dev.mjs new file mode 100755 index 000000000..35a9fa16c --- /dev/null +++ b/packages/demobank-ui/dev.mjs @@ -0,0 +1,30 @@ +#!/usr/bin/env node +/* + This file is part of GNU Taler + (C) 2022 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ + +import { serve } from "@gnu-taler/web-util/lib/index.node"; +import esbuild from "esbuild"; +import { buildConfig } from "./build.mjs"; + +buildConfig.inject = ['./node_modules/@gnu-taler/web-util/lib/live-reload.mjs'] + +serve({ + folder: './dist', + port: 8080, + source: './src', + development: true, + onUpdate: async () => esbuild.build(buildConfig) +}) diff --git a/packages/demobank-ui/package.json b/packages/demobank-ui/package.json index cc8048a0f..41031977f 100644 --- a/packages/demobank-ui/package.json +++ b/packages/demobank-ui/package.json @@ -11,13 +11,13 @@ }, "dependencies": { "@gnu-taler/taler-util": "workspace:*", + "@gnu-taler/web-util": "workspace:*", "date-fns": "2.29.3", "history": "4.10.1", "jed": "1.1.1", - "preact": "10.6.5", + "preact": "10.11.3", "preact-router": "3.2.1", "qrcode-generator": "^1.4.4", - "react": "npm:@preact/compat@^17.1.2", "swr": "1.3.0" }, "devDependencies": { @@ -30,11 +30,10 @@ "bulma-checkbox": "^1.1.1", "bulma-radio": "^1.1.1", "esbuild": "^0.15.12", - "esbuild-sass-plugin": "^2.4.0", "eslint": "^8.26.0", "eslint-config-preact": "^1.2.0", "po2json": "^0.4.5", - "sass": "1.32.13", + "sass": "1.56.1", "typescript": "^4.4.4" } } diff --git a/packages/demobank-ui/src/components/menu/SideBar.tsx b/packages/demobank-ui/src/components/menu/SideBar.tsx index d7833df5a..7bfba2a75 100644 --- a/packages/demobank-ui/src/components/menu/SideBar.tsx +++ b/packages/demobank-ui/src/components/menu/SideBar.tsx @@ -20,7 +20,7 @@ */ import { h, VNode } from "preact"; -import { Translate } from "../../i18n"; +import { useTranslationContext } from "../../context/translation.js"; interface Props { mobile?: boolean; @@ -31,6 +31,7 @@ export function Sidebar({ mobile }: Props): VNode { const config = { version: "none" }; // FIXME: add replacement for __VERSION__ with the current version const process = { env: { __VERSION__: "0.0.0" } }; + const { i18n } = useTranslationContext(); return ( <aside class="aside is-placed-left is-expanded"> @@ -49,20 +50,20 @@ export function Sidebar({ mobile }: Props): VNode { </div> <div class="menu is-menu-main"> <p class="menu-label"> - <Translate>Bank menu</Translate> + <i18n.Translate>Bank menu</i18n.Translate> </p> <ul class="menu-list"> <li> <div class="ml-4"> <span class="menu-item-label"> - <Translate>Select option1</Translate> + <i18n.Translate>Select option1</i18n.Translate> </span> </div> </li> <li> <div class="ml-4"> <span class="menu-item-label"> - <Translate>Select option2</Translate> + <i18n.Translate>Select option2</i18n.Translate> </span> </div> </li> diff --git a/packages/demobank-ui/src/components/picker/DurationPicker.tsx b/packages/demobank-ui/src/components/picker/DurationPicker.tsx index 94f2326bc..b8a7671c3 100644 --- a/packages/demobank-ui/src/components/picker/DurationPicker.tsx +++ b/packages/demobank-ui/src/components/picker/DurationPicker.tsx @@ -21,7 +21,7 @@ import { h, VNode } from "preact"; import { useState } from "preact/hooks"; -import { useTranslator } from "../../i18n"; +import { useTranslationContext } from "../../context/translation.js"; import "../../scss/DurationPicker.scss"; export interface Props { @@ -46,13 +46,13 @@ export function DurationPicker({ const ms = ss * 60; const hs = ms * 60; const ds = hs * 24; - const i18n = useTranslator(); + const { i18n } = useTranslationContext(); return ( <div class="rdp-picker"> {days && ( <DurationColumn - unit={i18n`days`} + unit={i18n.str`days`} max={99} value={Math.floor(value / ds)} onDecrease={value >= ds ? () => onChange(value - ds) : undefined} @@ -62,7 +62,7 @@ export function DurationPicker({ )} {hours && ( <DurationColumn - unit={i18n`hours`} + unit={i18n.str`hours`} max={23} min={1} value={Math.floor(value / hs) % 24} @@ -73,7 +73,7 @@ export function DurationPicker({ )} {minutes && ( <DurationColumn - unit={i18n`minutes`} + unit={i18n.str`minutes`} max={59} min={1} value={Math.floor(value / ms) % 60} @@ -84,7 +84,7 @@ export function DurationPicker({ )} {seconds && ( <DurationColumn - unit={i18n`seconds`} + unit={i18n.str`seconds`} max={59} value={Math.floor(value / ss) % 60} onDecrease={value >= ss ? () => onChange(value - ss) : undefined} diff --git a/packages/demobank-ui/src/context/translation.ts b/packages/demobank-ui/src/context/translation.ts index a411ecb16..a50f81b86 100644 --- a/packages/demobank-ui/src/context/translation.ts +++ b/packages/demobank-ui/src/context/translation.ts @@ -19,27 +19,42 @@ * @author Sebastian Javier Marchano (sebasjm) */ +import { i18n, setupI18n } from "@gnu-taler/taler-util"; import { createContext, h, VNode } from "preact"; import { useContext, useEffect } from "preact/hooks"; -import { useLang } from "../hooks/index.js"; -import * as jedLib from "jed"; +import { useLang } from "../hooks/useLang.js"; import { strings } from "../i18n/strings.js"; interface Type { lang: string; - handler: any; + supportedLang: { [id in keyof typeof supportedLang]: string }; changeLanguage: (l: string) => void; + i18n: typeof i18n; + isSaved: boolean; } + +const supportedLang = { + es: "Español [es]", + ja: "日本語 [ja]", + en: "English [en]", + fr: "Français [fr]", + de: "Deutsch [de]", + sv: "Svenska [sv]", + it: "Italiano [it]", + // ko: "한국어 [ko]", + // ru: "Ру́сский язы́к [ru]", + tr: "Türk [tr]", + navigator: "Defined by navigator", +}; + const initial = { lang: "en", - handler: null, + supportedLang, changeLanguage: () => { - /** - * This function will be replaced by one with - * the same signature _but_ coming from the state. - * FIXME: clarify this design. - */ + // do not change anything }, + i18n, + isSaved: false, }; const Context = createContext<Type>(initial); @@ -55,14 +70,23 @@ export const TranslationProvider = ({ children, forceLang, }: Props): VNode => { - const [lang, changeLanguage] = useLang(initial); + const [lang, changeLanguage, isSaved] = useLang(initial); useEffect(() => { - if (forceLang) changeLanguage(forceLang); + if (forceLang) { + changeLanguage(forceLang); + } }); - console.log("lang store", strings); - const handler = new jedLib.Jed(strings[lang] || strings["en"]); + useEffect(() => { + setupI18n(lang, strings); + }, [lang]); + if (forceLang) { + setupI18n(forceLang, strings); + } else { + setupI18n(lang, strings); + } + return h(Context.Provider, { - value: { lang, handler, changeLanguage }, + value: { lang, changeLanguage, supportedLang, i18n, isSaved }, children, }); }; diff --git a/packages/demobank-ui/src/hooks/index.ts b/packages/demobank-ui/src/hooks/index.ts index 94e66e5e3..b4191d182 100644 --- a/packages/demobank-ui/src/hooks/index.ts +++ b/packages/demobank-ui/src/hooks/index.ts @@ -19,7 +19,8 @@ * @author Sebastian Javier Marchano (sebasjm) */ -import { StateUpdater, useState } from "preact/hooks"; +import { StateUpdater } from "preact/hooks"; +import { useLocalStorage, useNotNullLocalStorage } from "./useLocalStorage.js"; export type ValueOrFunction<T> = T | ((p: T) => T); const calculateRootPath = () => { @@ -68,79 +69,3 @@ export function useBackendInstanceToken( return [token, setToken]; } - -export function useLang(initial?: string): [string, StateUpdater<string>] { - const browserLang = - typeof window !== "undefined" - ? navigator.language || (navigator as any).userLanguage - : undefined; - const defaultLang = (browserLang || initial || "en").substring(0, 2); - const [value, setValue] = useNotNullLocalStorage( - "lang-preference", - defaultLang, - ); - function updateValue(newValue: string | ((v: string) => string)) { - if (document.body.parentElement) { - const htmlElement = document.body.parentElement; - if (typeof newValue === "string") { - htmlElement.lang = newValue; - setValue(newValue); - } else if (typeof newValue === "function") - setValue((old) => { - const nv = newValue(old); - htmlElement.lang = nv; - return nv; - }); - } else setValue(newValue); - } - return [value, updateValue]; -} - -export function useLocalStorage( - key: string, - initialValue?: string, -): [string | undefined, StateUpdater<string | undefined>] { - const [storedValue, setStoredValue] = useState<string | undefined>( - (): string | undefined => { - return typeof window !== "undefined" - ? window.localStorage.getItem(key) || initialValue - : initialValue; - }, - ); - - const setValue = ( - value?: string | ((val?: string) => string | undefined), - ) => { - setStoredValue((p) => { - const toStore = value instanceof Function ? value(p) : value; - if (typeof window !== "undefined") - if (!toStore) window.localStorage.removeItem(key); - else window.localStorage.setItem(key, toStore); - - return toStore; - }); - }; - - return [storedValue, setValue]; -} - -export function useNotNullLocalStorage( - key: string, - initialValue: string, -): [string, StateUpdater<string>] { - const [storedValue, setStoredValue] = useState<string>((): string => { - return typeof window !== "undefined" - ? window.localStorage.getItem(key) || initialValue - : initialValue; - }); - - const setValue = (value: string | ((val: string) => string)) => { - const valueToStore = value instanceof Function ? value(storedValue) : value; - setStoredValue(valueToStore); - if (typeof window !== "undefined") - if (!valueToStore) window.localStorage.removeItem(key); - else window.localStorage.setItem(key, valueToStore); - }; - - return [storedValue, setValue]; -} diff --git a/packages/demobank-ui/src/hooks/useLang.ts b/packages/demobank-ui/src/hooks/useLang.ts new file mode 100644 index 000000000..5b02c5255 --- /dev/null +++ b/packages/demobank-ui/src/hooks/useLang.ts @@ -0,0 +1,30 @@ +/* + This file is part of GNU Anastasis + (C) 2021-2022 Anastasis SARL + + GNU Anastasis is free software; you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Anastasis 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 Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License along with + GNU Anastasis; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ + +import { useNotNullLocalStorage } from "./useLocalStorage.js"; + +function getBrowserLang(): string | undefined { + if (window.navigator.languages) return window.navigator.languages[0]; + if (window.navigator.language) return window.navigator.language; + return undefined; +} + +export function useLang( + initial?: string, +): [string, (s: string) => void, boolean] { + const defaultLang = (getBrowserLang() || initial || "en").substring(0, 2); + return useNotNullLocalStorage("lang-preference", defaultLang); +} diff --git a/packages/demobank-ui/src/hooks/useLocalStorage.ts b/packages/demobank-ui/src/hooks/useLocalStorage.ts new file mode 100644 index 000000000..ed5b491f2 --- /dev/null +++ b/packages/demobank-ui/src/hooks/useLocalStorage.ts @@ -0,0 +1,80 @@ +/* + This file is part of GNU Anastasis + (C) 2021-2022 Anastasis SARL + + GNU Anastasis is free software; you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Anastasis 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 Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License along with + GNU Anastasis; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ + +/** + * + * @author Sebastian Javier Marchano (sebasjm) + */ + +import { StateUpdater, useState } from "preact/hooks"; + +export function useLocalStorage( + key: string, + initialValue?: string, +): [string | undefined, StateUpdater<string | undefined>] { + const [storedValue, setStoredValue] = useState<string | undefined>( + (): string | undefined => { + return typeof window !== "undefined" + ? window.localStorage.getItem(key) || initialValue + : initialValue; + }, + ); + + const setValue = ( + value?: string | ((val?: string) => string | undefined), + ): void => { + setStoredValue((p) => { + const toStore = value instanceof Function ? value(p) : value; + if (typeof window !== "undefined") { + if (!toStore) { + window.localStorage.removeItem(key); + } else { + window.localStorage.setItem(key, toStore); + } + } + return toStore; + }); + }; + + return [storedValue, setValue]; +} + +//TODO: merge with the above function +export function useNotNullLocalStorage( + key: string, + initialValue: string, +): [string, StateUpdater<string>, boolean] { + const [storedValue, setStoredValue] = useState<string>((): string => { + return typeof window !== "undefined" + ? window.localStorage.getItem(key) || initialValue + : initialValue; + }); + + const setValue = (value: string | ((val: string) => string)): void => { + const valueToStore = value instanceof Function ? value(storedValue) : value; + setStoredValue(valueToStore); + if (typeof window !== "undefined") { + if (!valueToStore) { + window.localStorage.removeItem(key); + } else { + window.localStorage.setItem(key, valueToStore); + } + } + }; + + const isSaved = window.localStorage.getItem(key) !== null; + return [storedValue, setValue, isSaved]; +} diff --git a/packages/demobank-ui/src/i18n/index.tsx b/packages/demobank-ui/src/i18n/index.tsx deleted file mode 100644 index 2489184b2..000000000 --- a/packages/demobank-ui/src/i18n/index.tsx +++ /dev/null @@ -1,201 +0,0 @@ -/* - 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/> - */ - -/** - * Translation helpers for React components and template literals. - */ - -/** - * 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[] - ): string { - const s = toI18nString(stringSeq); - if (!s) return s; - const tr = jed - .translate(s) - .ifPlural(1, s) - .fetch(...values); - return tr; - }; -} - -/** - * Convert template strings to a msgid - */ -function toI18nString(stringSeq: ReadonlyArray<string>): string { - let s = ""; - for (let i = 0; i < stringSeq.length; i++) { - s += stringSeq[i]; - if (i < stringSeq.length - 1) s += `%${i + 1}$s`; - } - 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/demobank-ui/src/index.html b/packages/demobank-ui/src/index.html index a2154429b..4b3c89a66 100644 --- a/packages/demobank-ui/src/index.html +++ b/packages/demobank-ui/src/index.html @@ -16,25 +16,26 @@ @author Sebastian Javier Marchano --> <!DOCTYPE html> -<html - lang="en" - class="has-aside-left has-aside-mobile-transition has-navbar-fixed-top has-aside-expanded" -> +<html lang="en"> <head> + <meta http-equiv="content-type" content="text/html; charset=utf-8" /> <meta charset="utf-8" /> <meta name="viewport" content="width=device-width,initial-scale=1" /> <meta name="mobile-web-app-capable" content="yes" /> <meta name="apple-mobile-web-app-capable" content="yes" /> - <link rel="icon" href="data:;base64,AAABAAEAEBAAAAEAIABoBAAAFgAAACgAAAAQAAAAIAAAAAEAIAAAAAAAAAQAABILAAASCwAAAAAAAAAAAAD///////////////////////////////////////////////////////////////////////////////////////////////////7//v38//78/P/+/fz//vz7///+/v/+/f3//vz7///+/v/+/fz//v38///////////////////////+/v3///7+/////////////////////////////////////////////////////////v3//v79///////+/v3///////r28v/ct5//06SG/9Gffv/Xqo7/7N/V/9e2nf/bsJb/6uDW/9Sskf/euKH/+/j2///////+/v3//////+3azv+/eE3/2rWd/9Kkhv/Vr5T/48i2/8J+VP/Qn3//3ryn/795Tf/WrpP/2LCW/8B6T//w4Nb///////Pn4P+/d0v/9u3n/+7d0v/EhV7//v///+HDr//fxLD/zph2/+TJt//8/Pv/woBX//Lm3f/y5dz/v3hN//bu6f/JjGn/4sW0///////Df1j/8OLZ//v6+P+/elH/+vj1//jy7f+/elL//////+zYzP/Eg13//////967p//MlHT/wn5X///////v4Nb/yY1s///////jw7H/06KG////////////z5t9/+fNvf//////x4pn//Pp4v/8+vn/w39X/8WEX///////5s/A/9CbfP//////27Oc/9y2n////////////9itlf/gu6f//////86Vdf/r2Mz//////8SCXP/Df1j//////+7d0v/KkG7//////+HBrf/VpYr////////////RnoH/5sq6///////Ii2n/8ubf//39/P/Cf1j/xohk/+bNvv//////wn5W//Tq4//58/D/wHxV//7+/f/59fH/v3xU//39/P/w4Nf/xIFb///////hw7H/yo9t/+/f1f/AeU3/+/n2/+nSxP/FhmD//////9qzm//Upon/4MSx/96+qf//////xINc/+3bz//48e3/v3hN//Pn3///////6M+//752S//gw6//06aK/8J+VP/kzLr/zZd1/8OCWv/q18r/17KZ/9Ooi//fv6r/v3dK/+vWyP///////v39///////27un/1aeK/9Opjv/m1cf/1KCC/9a0nP/n08T/0Jx8/82YdP/QnHz/16yR//jx7P///////v39///////+/f3///7+///////+//7//v7+///////+/v7//v/+/////////////////////////v7//v79///////////////////+/v/+/Pv//v39///+/v/+/Pv///7+//7+/f/+/Pv//v39//79/P/+/Pv///7+////////////AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==" /> <link rel="shortcut icon" href="data:image/x-icon;," type="image/x-icon" /> + <title>Demobank</title> + <!-- Optional customization script. --> + <script src="demobank-ui-settings.js"></script> + <!-- Entry point for the demobank SPA. --> + <script type="module" src="index.js"></script> + <link rel="stylesheet" href="index.css" /> </head> - <body> <div id="app"></div> - <script type="module" src="index.tsx"></script> </body> </html> diff --git a/packages/demobank-ui/src/index.tsx b/packages/demobank-ui/src/index.tsx index 4302bb33b..0b88b0393 100644 --- a/packages/demobank-ui/src/index.tsx +++ b/packages/demobank-ui/src/index.tsx @@ -1,6 +1,7 @@ import App from "./components/app.js"; export default App; import { render, h } from "preact"; +import "./scss/main.scss"; const app = document.getElementById("app"); diff --git a/packages/demobank-ui/src/pages/home/QrCodeSection.stories.tsx b/packages/demobank-ui/src/pages/home/QrCodeSection.stories.tsx new file mode 100644 index 000000000..521d4255e --- /dev/null +++ b/packages/demobank-ui/src/pages/home/QrCodeSection.stories.tsx @@ -0,0 +1,33 @@ +/* + This file is part of GNU Taler + (C) 2022 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ + +/** + * + * @author Sebastian Javier Marchano (sebasjm) + */ + +import { QrCodeSection } from "./QrCodeSection.js"; + +export default { + title: "Qr Code Selection", +}; + +export const SimpleExample = { + component: QrCodeSection, + props: { + talerWithdrawUri: "taler://withdraw/asdasdasd", + }, +}; diff --git a/packages/demobank-ui/src/pages/home/QrCodeSection.tsx b/packages/demobank-ui/src/pages/home/QrCodeSection.tsx new file mode 100644 index 000000000..1d7b3db10 --- /dev/null +++ b/packages/demobank-ui/src/pages/home/QrCodeSection.tsx @@ -0,0 +1,55 @@ +/* + This file is part of GNU Taler + (C) 2022 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ + +import { h, VNode } from "preact"; +import { useEffect } from "preact/hooks"; +import { QR } from "../../components/QR.js"; +import { useTranslationContext } from "../../context/translation.js"; + +export function QrCodeSection({ + talerWithdrawUri, + abortButton, +}: { + talerWithdrawUri: string; + abortButton: h.JSX.Element; +}): VNode { + const { i18n } = useTranslationContext(); + useEffect(() => { + //Taler Wallet WebExtension is listening to headers response and tab updates. + //In the SPA there is no header response with the Taler URI so + //this hack manually triggers the tab update after the QR is in the DOM. + window.location.hash = `/account/${new Date().getTime()}`; + }, []); + + return ( + <section id="main" class="content"> + <h1 class="nav">{i18n.str`Transfer to Taler Wallet`}</h1> + <article> + <div class="qr-div"> + <p>{i18n.str`Use this QR code to withdraw to your mobile wallet:`}</p> + {QR({ text: talerWithdrawUri })} + <p> + Click{" "} + <a id="linkqr" href={talerWithdrawUri}>{i18n.str`this link`}</a> to + open your Taler wallet! + </p> + <br /> + {abortButton} + </div> + </article> + </section> + ); +} diff --git a/packages/demobank-ui/src/pages/home/index.stories.tsx b/packages/demobank-ui/src/pages/home/index.stories.tsx new file mode 100644 index 000000000..e9ac00a76 --- /dev/null +++ b/packages/demobank-ui/src/pages/home/index.stories.tsx @@ -0,0 +1 @@ +export * as qr from "./QrCodeSection.stories.js"; diff --git a/packages/demobank-ui/src/pages/home/index.tsx b/packages/demobank-ui/src/pages/home/index.tsx index 8f522c07c..8b2ffefac 100644 --- a/packages/demobank-ui/src/pages/home/index.tsx +++ b/packages/demobank-ui/src/pages/home/index.tsx @@ -27,13 +27,16 @@ import { } from "preact/hooks"; import talerLogo from "../../assets/logo-white.svg"; import { LangSelectorLikePy as LangSelector } from "../../components/menu/LangSelector.js"; -import { QR } from "../../components/QR.js"; -import { useLocalStorage, useNotNullLocalStorage } from "../../hooks/index.js"; -import { Translate, useTranslator } from "../../i18n/index.js"; -import "../../scss/main.scss"; +import { + useLocalStorage, + useNotNullLocalStorage, +} from "../../hooks/useLocalStorage.js"; +// import { Translate, useTranslator } from "../../i18n/index.js"; +import { useTranslationContext } from "../../context/translation.js"; import { Amounts, HttpStatusCode, parsePaytoUri } from "@gnu-taler/taler-util"; import { createHashHistory } from "history"; import Router, { Route, route } from "preact-router"; +import { QrCodeSection } from "./QrCodeSection.js"; interface BankUiSettings { allowRegistrations: boolean; @@ -987,7 +990,7 @@ async function registrationCall( function ErrorBanner(Props: any): VNode | null { const [pageState, pageStateSetter] = Props.pageState; - // const i18n = useTranslator(); + // const { i18n } = useTranslationContext(); if (!pageState.error) return null; const rval = ( @@ -1041,7 +1044,7 @@ function StatusBanner(Props: any): VNode | null { } function BankFrame(Props: any): VNode { - const i18n = useTranslator(); + const { i18n } = useTranslationContext(); const [pageState, pageStateSetter] = useContext(PageContext); console.log("BankFrame state", pageState); const logOut = ( @@ -1062,7 +1065,7 @@ function BankFrame(Props: any): VNode { }; }); }} - >{i18n`Logout`}</a> + >{i18n.str`Logout`}</a> </div> ); @@ -1080,7 +1083,7 @@ function BankFrame(Props: any): VNode { class="demobar" style="display: flex; flex-direction: row; justify-content: space-between;" > - <a href="#main" class="skip">{i18n`Skip to main content`}</a> + <a href="#main" class="skip">{i18n.str`Skip to main content`}</a> <div style="max-width: 50em; margin-left: 2em;"> <h1> <span class="it"> @@ -1089,7 +1092,7 @@ function BankFrame(Props: any): VNode { </h1> {maybeDemoContent( <p> - <Translate> + <i18n.Translate> This part of the demo shows how a bank that supports Taler directly would work. In addition to using your own bank account, you can also see the transaction history of some{" "} @@ -1100,14 +1103,14 @@ function BankFrame(Props: any): VNode { Public Accounts </a> . - </Translate> + </i18n.Translate> </p>, )} </div> <a href="https://taler.net/"> <img src={talerLogo} - alt={i18n`Taler logo`} + alt={i18n.str`Taler logo`} height="100" width="224" style="margin: 2em 2em" @@ -1168,7 +1171,7 @@ function PaytoWireTransfer(Props: any): VNode { const [rawPaytoInput, rawPaytoInputSetter] = useState<string | undefined>( undefined, ); - const i18n = useTranslator(); + const { i18n } = useTranslationContext(); const { focus, backendState } = Props; const amountRegex = "^[0-9]+(.[0-9]+)?$"; const ibanRegex = "^[A-Z][A-Z][0-9]+$"; @@ -1193,17 +1196,17 @@ function PaytoWireTransfer(Props: any): VNode { ? undefined : undefinedIfEmpty({ iban: !submitData.iban - ? i18n`Missing IBAN` + ? i18n.str`Missing IBAN` : !/^[A-Z0-9]*$/.test(submitData.iban) - ? i18n`IBAN should have just uppercased letters and numbers` + ? i18n.str`IBAN should have just uppercased letters and numbers` : undefined, - subject: !submitData.subject ? i18n`Missing subject` : undefined, + subject: !submitData.subject ? i18n.str`Missing subject` : undefined, amount: !submitData.amount - ? i18n`Missing amount` + ? i18n.str`Missing amount` : !(parsedAmount = Amounts.parse(`${currency}:${submitData.amount}`)) - ? i18n`Amount is not valid` + ? i18n.str`Amount is not valid` : Amounts.isZero(parsedAmount) - ? i18n`Should be greater than 0` + ? i18n.str`Should be greater than 0` : undefined, }); @@ -1212,7 +1215,7 @@ function PaytoWireTransfer(Props: any): VNode { <div> <div class="pure-form" name="wire-transfer-form"> <p> - <label for="iban">{i18n`Receiver IBAN:`}</label> + <label for="iban">{i18n.str`Receiver IBAN:`}</label> <input ref={ref} type="text" @@ -1235,7 +1238,7 @@ function PaytoWireTransfer(Props: any): VNode { isDirty={submitData?.iban !== undefined} /> <br /> - <label for="subject">{i18n`Transfer subject:`}</label> + <label for="subject">{i18n.str`Transfer subject:`}</label> <input type="text" name="subject" @@ -1256,7 +1259,7 @@ function PaytoWireTransfer(Props: any): VNode { isDirty={submitData?.subject !== undefined} /> <br /> - <label for="amount">{i18n`Amount:`}</label> + <label for="amount">{i18n.str`Amount:`}</label> <input type="number" name="amount" @@ -1309,7 +1312,7 @@ function PaytoWireTransfer(Props: any): VNode { ...prevState, error: { - title: i18n`Field(s) missing.`, + title: i18n.str`Field(s) missing.`, }, })); return; @@ -1358,7 +1361,7 @@ function PaytoWireTransfer(Props: any): VNode { })); }} > - {i18n`Want to try the raw payto://-format?`} + {i18n.str`Want to try the raw payto://-format?`} </a> </p> </div> @@ -1366,18 +1369,18 @@ function PaytoWireTransfer(Props: any): VNode { const errorsPayto = undefinedIfEmpty({ rawPaytoInput: !rawPaytoInput - ? i18n`Missing payto address` + ? i18n.str`Missing payto address` : !parsePaytoUri(rawPaytoInput) - ? i18n`Payto does not follow the pattern` + ? i18n.str`Payto does not follow the pattern` : undefined, }); return ( <div> - <p>{i18n`Transfer money to account identified by payto:// URI:`}</p> + <p>{i18n.str`Transfer money to account identified by payto:// URI:`}</p> <div class="pure-form" name="payto-form"> <p> - <label for="address">{i18n`payto URI:`}</label> + <label for="address">{i18n.str`payto URI:`}</label> <input name="address" type="text" @@ -1386,7 +1389,7 @@ function PaytoWireTransfer(Props: any): VNode { id="address" value={rawPaytoInput ?? ""} required - placeholder={i18n`payto address`} + placeholder={i18n.str`payto address`} // pattern={`payto://iban/[A-Z][A-Z][0-9]+?message=[a-zA-Z0-9 ]+&amount=${currency}:[0-9]+(.[0-9]+)?`} onInput={(e): void => { rawPaytoInputSetter(e.currentTarget.value); @@ -1410,7 +1413,7 @@ function PaytoWireTransfer(Props: any): VNode { class="pure-button pure-button-primary" type="submit" disabled={!!errorsPayto} - value={i18n`Send`} + value={i18n.str`Send`} onClick={async () => { // empty string evaluates to false. if (!rawPaytoInput) { @@ -1444,7 +1447,7 @@ function PaytoWireTransfer(Props: any): VNode { })); }} > - {i18n`Use wire-transfer form?`} + {i18n.str`Use wire-transfer form?`} </a> </p> </div> @@ -1459,7 +1462,7 @@ function PaytoWireTransfer(Props: any): VNode { function TalerWithdrawalConfirmationQuestion(Props: any): VNode { const [pageState, pageStateSetter] = useContext(PageContext); const { backendState } = Props; - const i18n = useTranslator(); + const { i18n } = useTranslationContext(); const captchaNumbers = { a: Math.floor(Math.random() * 10), b: Math.floor(Math.random() * 10), @@ -1468,15 +1471,15 @@ function TalerWithdrawalConfirmationQuestion(Props: any): VNode { return ( <Fragment> - <h1 class="nav">{i18n`Confirm Withdrawal`}</h1> + <h1 class="nav">{i18n.str`Confirm Withdrawal`}</h1> <article> <div class="challenge-div"> <form class="challenge-form"> <div class="pure-form" id="captcha" name="capcha-form"> - <h2>{i18n`Authorize withdrawal by solving challenge`}</h2> + <h2>{i18n.str`Authorize withdrawal by solving challenge`}</h2> <p> <label for="answer"> - {i18n`What is`} + {i18n.str`What is`} <em> {captchaNumbers.a} + {captchaNumbers.b} </em> @@ -1514,12 +1517,12 @@ function TalerWithdrawalConfirmationQuestion(Props: any): VNode { ...prevState, error: { - title: i18n`Answer is wrong.`, + title: i18n.str`Answer is wrong.`, }, })); }} > - {i18n`Confirm`} + {i18n.str`Confirm`} </button> <button @@ -1532,18 +1535,18 @@ function TalerWithdrawalConfirmationQuestion(Props: any): VNode { ) } > - {i18n`Cancel`} + {i18n.str`Cancel`} </button> </p> </div> </form> <div class="hint"> <p> - <Translate> + <i18n.Translate> A this point, a <b>real</b> bank would ask for an additional authentication proof (PIN/TAN, one time password, ..), instead of a simple calculation. - </Translate> + </i18n.Translate> </p> </div> </div> @@ -1552,40 +1555,6 @@ function TalerWithdrawalConfirmationQuestion(Props: any): VNode { ); } -function QrCodeSection({ - talerWithdrawUri, - abortButton, -}: { - talerWithdrawUri: string; - abortButton: h.JSX.Element; -}): VNode { - const i18n = useTranslator(); - useEffect(() => { - //Taler Wallet WebExtension is listening to headers response and tab updates. - //In the SPA there is no header response with the Taler URI so - //this hack manually triggers the tab update after the QR is in the DOM. - window.location.hash = `/account/${new Date().getTime()}`; - }, []); - - return ( - <section id="main" class="content"> - <h1 class="nav">{i18n`Transfer to Taler Wallet`}</h1> - <article> - <div class="qr-div"> - <p>{i18n`Use this QR code to withdraw to your mobile wallet:`}</p> - {QR({ text: talerWithdrawUri })} - <p> - Click <a id="linkqr" href={talerWithdrawUri}>{i18n`this link`}</a>{" "} - to open your Taler wallet! - </p> - <br /> - {abortButton} - </div> - </article> - </section> - ); -} - /** * Offer the QR code (and a clickable taler://-link) to * permit the passing of exchange and reserve details to @@ -1595,7 +1564,7 @@ function TalerWithdrawalQRCode(Props: any): VNode { // turns true when the wallet POSTed the reserve details: const [pageState, pageStateSetter] = useContext(PageContext); const { withdrawalId, talerWithdrawUri, accountLabel, backendState } = Props; - const i18n = useTranslator(); + const { i18n } = useTranslationContext(); const abortButton = ( <a class="pure-button btn-cancel" @@ -1609,7 +1578,7 @@ function TalerWithdrawalQRCode(Props: any): VNode { }; }); }} - >{i18n`Abort`}</a> + >{i18n.str`Abort`}</a> ); console.log(`Showing withdraw URI: ${talerWithdrawUri}`); @@ -1629,7 +1598,7 @@ function TalerWithdrawalQRCode(Props: any): VNode { ...prevState, error: { - title: i18n`withdrawal (${withdrawalId}) was never (correctly) created at the bank...`, + title: i18n.str`withdrawal (${withdrawalId}) was never (correctly) created at the bank...`, }, })); return ( @@ -1643,7 +1612,7 @@ function TalerWithdrawalQRCode(Props: any): VNode { // data didn't arrive yet and wallet didn't communicate: if (typeof data === "undefined") - return <p>{i18n`Waiting the bank to create the operation...`}</p>; + return <p>{i18n.str`Waiting the bank to create the operation...`}</p>; /** * Wallet didn't communicate withdrawal details yet: @@ -1657,7 +1626,7 @@ function TalerWithdrawalQRCode(Props: any): VNode { withdrawalInProgress: false, error: { - title: i18n`This withdrawal was aborted!`, + title: i18n.str`This withdrawal was aborted!`, }, }; }); @@ -1680,7 +1649,7 @@ function TalerWithdrawalQRCode(Props: any): VNode { function WalletWithdraw(Props: any): VNode { const { backendState, pageStateSetter, focus } = Props; const currency = useContext(CurrencyContext); - const i18n = useTranslator(); + const { i18n } = useTranslationContext(); let submitAmount = "5.00"; const amountRegex = "^[0-9]+(.[0-9]+)?$"; @@ -1691,7 +1660,8 @@ function WalletWithdraw(Props: any): VNode { return ( <div id="reserve-form" class="pure-form" name="tform"> <p> - <label for="withdraw-amount">{i18n`Amount to withdraw:`}</label> + <label for="withdraw-amount">{i18n.str`Amount to withdraw:`}</label> + <input type="number" ref={ref} @@ -1724,7 +1694,7 @@ function WalletWithdraw(Props: any): VNode { id="select-exchange" class="pure-button pure-button-primary" type="submit" - value={i18n`Withdraw`} + value={i18n.str`Withdraw`} onClick={() => { submitAmount = validateAmount(submitAmount); /** @@ -1753,7 +1723,7 @@ function WalletWithdraw(Props: any): VNode { function PaymentOptions(Props: any): VNode { const { backendState, pageStateSetter, focus } = Props; const currency = useContext(CurrencyContext); - const i18n = useTranslator(); + const { i18n } = useTranslationContext(); const [tab, setTab] = useState<"charge-wallet" | "wire-transfer">( "charge-wallet", @@ -1769,7 +1739,7 @@ function PaymentOptions(Props: any): VNode { setTab("charge-wallet"); }} > - {i18n`Obtain digital cash`} + {i18n.str`Obtain digital cash`} </button> <button class={tab === "wire-transfer" ? "tablinks active" : "tablinks"} @@ -1777,12 +1747,12 @@ function PaymentOptions(Props: any): VNode { setTab("wire-transfer"); }} > - {i18n`Transfer to bank account`} + {i18n.str`Transfer to bank account`} </button> </div> {tab === "charge-wallet" && ( <div id="charge-wallet" class="tabcontent active"> - <h3>{i18n`Obtain digital cash`}</h3> + <h3>{i18n.str`Obtain digital cash`}</h3> <WalletWithdraw backendState={backendState} focus @@ -1792,7 +1762,7 @@ function PaymentOptions(Props: any): VNode { )} {tab === "wire-transfer" && ( <div id="wire-transfer" class="tabcontent active"> - <h3>{i18n`Transfer to bank account`}</h3> + <h3>{i18n.str`Transfer to bank account`}</h3> <PaytoWireTransfer backendState={backendState} focus @@ -1807,7 +1777,7 @@ function PaymentOptions(Props: any): VNode { function RegistrationButton(Props: any): VNode { const { backendStateSetter, pageStateSetter } = Props; - const i18n = useTranslator(); + const { i18n } = useTranslationContext(); if (bankUiSettings.allowRegistrations) return ( <button @@ -1816,7 +1786,7 @@ function RegistrationButton(Props: any): VNode { route("/register"); }} > - {i18n`Register`} + {i18n.str`Register`} </button> ); @@ -1834,7 +1804,7 @@ function undefinedIfEmpty<T extends object>(obj: T): T | undefined { function LoginForm(Props: any): VNode { const { backendStateSetter, pageStateSetter } = Props; const [submitData, submitDataSetter] = useCredentialsRequestType(); - const i18n = useTranslator(); + const { i18n } = useTranslationContext(); const ref = useRef<HTMLInputElement>(null); useEffect(() => { ref.current?.focus(); @@ -1843,17 +1813,17 @@ function LoginForm(Props: any): VNode { const errors = !submitData ? undefined : undefinedIfEmpty({ - username: !submitData.username ? i18n`Missing username` : undefined, - password: !submitData.password ? i18n`Missing password` : undefined, + username: !submitData.username ? i18n.str`Missing username` : undefined, + password: !submitData.password ? i18n.str`Missing password` : undefined, }); return ( <div class="login-div"> <form action="javascript:void(0);" class="login-form"> <div class="pure-form"> - <h2>{i18n`Please login!`}</h2> + <h2>{i18n.str`Please login!`}</h2> <p class="unameFieldLabel loginFieldLabel formFieldLabel"> - <label for="username">{i18n`Username:`}</label> + <label for="username">{i18n.str`Username:`}</label> </p> <input ref={ref} @@ -1872,7 +1842,7 @@ function LoginForm(Props: any): VNode { }} /> <p class="passFieldLabel loginFieldLabel formFieldLabel"> - <label for="password">{i18n`Password:`}</label> + <label for="password">{i18n.str`Password:`}</label> </p> <input type="password" @@ -1919,7 +1889,7 @@ function LoginForm(Props: any): VNode { }); }} > - {i18n`Login`} + {i18n.str`Login`} </button> {RegistrationButton(Props)} </div> @@ -1935,30 +1905,30 @@ function RegistrationForm(Props: any): VNode { // eslint-disable-next-line @typescript-eslint/no-unused-vars const [pageState, pageStateSetter] = useContext(PageContext); const [submitData, submitDataSetter] = useCredentialsRequestType(); - const i18n = useTranslator(); + const { i18n } = useTranslationContext(); const errors = !submitData ? undefined : undefinedIfEmpty({ - username: !submitData.username ? i18n`Missing username` : undefined, - password: !submitData.password ? i18n`Missing password` : undefined, + username: !submitData.username ? i18n.str`Missing username` : undefined, + password: !submitData.password ? i18n.str`Missing password` : undefined, repeatPassword: !submitData.repeatPassword - ? i18n`Missing password` + ? i18n.str`Missing password` : submitData.repeatPassword !== submitData.password - ? i18n`Password don't match` + ? i18n.str`Password don't match` : undefined, }); return ( <Fragment> - <h1 class="nav">{i18n`Welcome to ${bankUiSettings.bankName}!`}</h1> + <h1 class="nav">{i18n.str`Welcome to ${bankUiSettings.bankName}!`}</h1> <article> <div class="register-div"> <form action="javascript:void(0);" class="register-form"> <div class="pure-form"> - <h2>{i18n`Please register!`}</h2> + <h2>{i18n.str`Please register!`}</h2> <p class="unameFieldLabel registerFieldLabel formFieldLabel"> - <label for="register-un">{i18n`Username:`}</label> + <label for="register-un">{i18n.str`Username:`}</label> </p> <input id="register-un" @@ -1976,7 +1946,7 @@ function RegistrationForm(Props: any): VNode { /> <br /> <p class="unameFieldLabel registerFieldLabel formFieldLabel"> - <label for="register-pw">{i18n`Password:`}</label> + <label for="register-pw">{i18n.str`Password:`}</label> </p> <input type="password" @@ -1993,7 +1963,7 @@ function RegistrationForm(Props: any): VNode { }} /> <p class="unameFieldLabel registerFieldLabel formFieldLabel"> - <label for="register-repeat">{i18n`Repeat Password:`}</label> + <label for="register-repeat">{i18n.str`Repeat Password:`}</label> </p> <input type="password" @@ -2012,7 +1982,7 @@ function RegistrationForm(Props: any): VNode { /> <br /> {/* - <label for="phone">{i18n`Phone number:`}</label> + <label for="phone">{i18n.str`Phone number:`}</label> // FIXME: add input validation (must start with +, otherwise only numbers) <input name="phone" @@ -2054,7 +2024,7 @@ function RegistrationForm(Props: any): VNode { }); }} > - {i18n`Register`} + {i18n.str`Register`} </button> {/* FIXME: should use a different color */} <button @@ -2068,7 +2038,7 @@ function RegistrationForm(Props: any): VNode { route("/account"); }} > - {i18n`Cancel`} + {i18n.str`Cancel`} </button> </div> </form> @@ -2083,7 +2053,7 @@ function RegistrationForm(Props: any): VNode { */ function Transactions(Props: any): VNode { const { pageNumber, accountLabel, balanceValue } = Props; - const i18n = useTranslator(); + const { i18n } = useTranslationContext(); const { data, error, mutate } = useSWR( `access-api/accounts/${accountLabel}/transactions?page=${pageNumber}`, ); @@ -2114,10 +2084,10 @@ function Transactions(Props: any): VNode { <table class="pure-table pure-table-striped"> <thead> <tr> - <th>{i18n`Date`}</th> - <th>{i18n`Amount`}</th> - <th>{i18n`Counterpart`}</th> - <th>{i18n`Subject`}</th> + <th>{i18n.str`Date`}</th> + <th>{i18n.str`Amount`}</th> + <th>{i18n.str`Counterpart`}</th> + <th>{i18n.str`Subject`}</th> </tr> </thead> <tbody> @@ -2178,7 +2148,7 @@ function Account(Props: any): VNode { talerWithdrawUri, timestamp, } = pageState; - const i18n = useTranslator(); + const { i18n } = useTranslationContext(); useEffect(() => { mutate(); }, [timestamp]); @@ -2206,7 +2176,7 @@ function Account(Props: any): VNode { isLoggedIn: false, error: { - title: i18n`Username or account label '${accountLabel}' not found. Won't login.`, + title: i18n.str`Username or account label '${accountLabel}' not found. Won't login.`, }, })); @@ -2233,7 +2203,7 @@ function Account(Props: any): VNode { isLoggedIn: false, error: { - title: i18n`Wrong credentials given.`, + title: i18n.str`Wrong credentials given.`, }, })); return <p>Wrong credentials...</p>; @@ -2244,7 +2214,7 @@ function Account(Props: any): VNode { isLoggedIn: false, error: { - title: i18n`Account information could not be retrieved.`, + title: i18n.str`Account information could not be retrieved.`, debug: JSON.stringify(error), }, })); @@ -2287,14 +2257,14 @@ function Account(Props: any): VNode { <BankFrame> <div> <h1 class="nav welcome-text"> - <Translate> + <i18n.Translate> Welcome, {accountLabel} ({getIbanFromPayto(data.paytoUri)})! - </Translate> + </i18n.Translate> </h1> </div> <section id="assets"> <div class="asset-summary"> - <h2>{i18n`Bank account balance`}</h2> + <h2>{i18n.str`Bank account balance`}</h2> <div class="large-amount amount"> {data.balance.credit_debit_indicator == "debit" ? <b>-</b> : null} <span class="value">{`${balanceValue}`}</span> @@ -2304,7 +2274,7 @@ function Account(Props: any): VNode { </section> <section id="payments"> <div class="payments"> - <h2>{i18n`Payments`}</h2> + <h2>{i18n.str`Payments`}</h2> {/* FIXME: turn into button! */} <CurrencyContext.Provider value={balance.currency}> {Props.children} @@ -2317,7 +2287,7 @@ function Account(Props: any): VNode { </section> <section id="main"> <article> - <h2>{i18n`Latest transactions:`}</h2> + <h2>{i18n.str`Latest transactions:`}</h2> <Transactions balanceValue={balanceValue} pageNumber="0" @@ -2379,7 +2349,7 @@ function SWRWithoutCredentials(Props: any): VNode { function PublicHistories(Props: any): VNode { const [showAccount, setShowAccount] = useShowPublicAccount(); const { data, error } = useSWR("access-api/public-accounts"); - const i18n = useTranslator(); + const { i18n } = useTranslationContext(); if (typeof error !== "undefined") { console.log("account error", error); @@ -2391,7 +2361,7 @@ function PublicHistories(Props: any): VNode { showPublicHistories: false, error: { - title: i18n`List of public accounts was not found.`, + title: i18n.str`List of public accounts was not found.`, debug: JSON.stringify(error), }, })); @@ -2403,7 +2373,7 @@ function PublicHistories(Props: any): VNode { showPublicHistories: false, error: { - title: i18n`List of public accounts could not be retrieved.`, + title: i18n.str`List of public accounts could not be retrieved.`, debug: JSON.stringify(error), }, })); @@ -2450,7 +2420,7 @@ function PublicHistories(Props: any): VNode { return ( <Fragment> - <h1 class="nav">{i18n`History of public accounts`}</h1> + <h1 class="nav">{i18n.str`History of public accounts`}</h1> <section id="main"> <article> <div class="pure-menu pure-menu-horizontal" name="accountMenu"> @@ -2471,7 +2441,7 @@ function PublicHistories(Props: any): VNode { function PublicHistoriesPage(): VNode { // const [backendState, backendStateSetter] = useBackendState(); const [pageState, pageStateSetter] = usePageState(); - // const i18n = useTranslator(); + // const { i18n } = useTranslationContext(); return ( <SWRWithoutCredentials baseUrl={getBankBackendBaseUrl()}> <PageContext.Provider value={[pageState, pageStateSetter]}> @@ -2499,12 +2469,12 @@ function PublicHistoriesPage(): VNode { function RegistrationPage(): VNode { const [backendState, backendStateSetter] = useBackendState(); const [pageState, pageStateSetter] = usePageState(); - const i18n = useTranslator(); + const { i18n } = useTranslationContext(); if (!bankUiSettings.allowRegistrations) { return ( <PageContext.Provider value={[pageState, pageStateSetter]}> <BankFrame> - <p>{i18n`Currently, the bank is not accepting new registrations!`}</p> + <p>{i18n.str`Currently, the bank is not accepting new registrations!`}</p> </BankFrame> </PageContext.Provider> ); @@ -2521,13 +2491,13 @@ function RegistrationPage(): VNode { function AccountPage(): VNode { const [backendState, backendStateSetter] = useBackendState(); const [pageState, pageStateSetter] = usePageState(); - const i18n = useTranslator(); + const { i18n } = useTranslationContext(); if (!pageState.isLoggedIn) { return ( <PageContext.Provider value={[pageState, pageStateSetter]}> <BankFrame> - <h1 class="nav">{i18n`Welcome to ${bankUiSettings.bankName}!`}</h1> + <h1 class="nav">{i18n.str`Welcome to ${bankUiSettings.bankName}!`}</h1> <LoginForm pageStateSetter={pageStateSetter} backendStateSetter={backendStateSetter} @@ -2543,7 +2513,7 @@ function AccountPage(): VNode { isLoggedIn: false, error: { - title: i18n`Page has a problem: logged in but backend state is lost.`, + title: i18n.str`Page has a problem: logged in but backend state is lost.`, }, })); return <p>Error: waiting for details...</p>; diff --git a/packages/demobank-ui/src/scss/main.scss b/packages/demobank-ui/src/scss/main.scss index ebe36b9b4..b92260af0 100644 --- a/packages/demobank-ui/src/scss/main.scss +++ b/packages/demobank-ui/src/scss/main.scss @@ -1,4 +1,4 @@ -@import "pure"; -@import "bank"; -@import "demo"; -@import "colors-bank"; +@use "pure"; +@use "bank"; +@use "demo"; +@use "colors-bank"; diff --git a/packages/demobank-ui/src/stories.tsx b/packages/demobank-ui/src/stories.tsx new file mode 100644 index 000000000..52d42577d --- /dev/null +++ b/packages/demobank-ui/src/stories.tsx @@ -0,0 +1,46 @@ +/* + This file is part of GNU Anastasis + (C) 2021-2022 Anastasis SARL + + GNU Anastasis is free software; you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Anastasis 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 Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License along with + GNU Anastasis; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ + +/** + * + * @author Sebastian Javier Marchano (sebasjm) + */ +import { strings } from "./i18n/strings.js"; + +import * as pages from "./pages/home/index.stories.js"; + +import { renderStories } from "@gnu-taler/web-util/lib/index.browser"; + +import "./scss/main.scss"; + +function SortStories(a: any, b: any): number { + return (a?.order ?? 0) - (b?.order ?? 0); +} + +function main(): void { + renderStories( + { pages }, + { + strings, + }, + ); +} + +if (document.readyState === "loading") { + document.addEventListener("DOMContentLoaded", main); +} else { + main(); +} diff --git a/packages/demobank-ui/static/index.html b/packages/demobank-ui/static/index.html deleted file mode 100644 index 0fa5215d3..000000000 --- a/packages/demobank-ui/static/index.html +++ /dev/null @@ -1,15 +0,0 @@ -<!DOCTYPE html> -<html> - <head> - <meta http-equiv="content-type" content="text/html; charset=utf-8" /> - <title>Demobank</title> - <!-- Optional customization script. --> - <script src="demobank-ui-settings.js"></script> - <!-- Entry point for the demobank SPA. --> - <script type="module" src="index.js"></script> - <link rel="stylesheet" href="index.css" /> - </head> - <body> - <div id="app"></div> - </body> -</html> |