using web-utils in anastasis-webui

This commit is contained in:
Sebastian 2022-12-06 11:21:12 -03:00
parent e382b02203
commit d3a6544bc5
No known key found for this signature in database
GPG Key ID: BE4FF68352439FC1
58 changed files with 430 additions and 1074 deletions

View File

@ -0,0 +1,147 @@
#!/usr/bin/env node
/*
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/>
*/
/* eslint-disable no-undef */
import esbuild from 'esbuild'
import fs from 'fs';
import path from "path"
import sass from "sass";
// eslint-disable-next-line no-undef
const BASE = process.cwd();
const preact = path.join(
BASE,
"node_modules",
"preact",
"compat",
"dist",
"compat.module.js",
);
const preactCompatPlugin = {
name: "preact-compat",
setup(build) {
build.onResolve({ filter: /^(react-dom|react)$/ }, (args) => {
return {
path: preact,
};
});
},
};
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()
let _package = JSON.parse(fs.readFileSync(path.join(BASE, 'package.json')));
function git_hash() {
const rev = fs.readFileSync(path.join(GIT_ROOT, '.git', 'HEAD')).toString().trim().split(/.*[: ]/).slice(-1)[0];
if (rev.indexOf('/') === -1) {
return rev;
} else {
return fs.readFileSync(path.join(GIT_ROOT, '.git', rev)).toString().trim();
}
}
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
}
});
},
};
function copyFilesPlugin(options) {
if (!options.basedir) {
options.basedir = process.cwd()
}
return {
name: "copy-files",
setup(build) {
build.onEnd(() => {
for (const fop of options) {
fs.copyFileSync(path.join(options.basedir, fop.src), path.join(options.basedir, fop.dest));
}
});
},
};
}
export const buildConfig = {
entryPoints: ['src/index.ts', 'src/stories.tsx'],
bundle: true,
outdir: 'dist',
minify: false,
loader: {
'.svg': 'dataurl',
'.ttf': 'file',
'.woff': 'file',
'.woff2': 'file',
'.eot': 'file',
},
target: [
'es6'
],
format: 'esm',
platform: 'browser',
sourcemap: true,
jsxFactory: 'h',
jsxFragment: 'Fragment',
define: {
'__VERSION__': `"${_package.version}"`,
'__GIT_HASH__': `"${GIT_HASH}"`,
},
plugins: [
preactCompatPlugin,
copyFilesPlugin([
{
src: "./src/index.html",
dest: "./dist/index.html",
},
]),
buildSassPlugin
],
}
await esbuild.build(buildConfig)

View File

@ -1,72 +0,0 @@
#!/usr/bin/env bash
echo clean
rm -rf dist
mkdir -p dist/fonts
cp \
src/scss/fonts/XRXV3I6Li01BKofINeaE.ttf \
src/scss/fonts/materialdesignicons-webfont-4.9.95.ttf \
src/scss/fonts/materialdesignicons-webfont-4.9.95.woff \
src/scss/fonts/materialdesignicons-webfont-4.9.95.woff2 \
dist/fonts
VERSION=$(jq -r .version package.json)
GIT_HASH=$(git rev-parse --short HEAD)
function build_css() {
pnpm exec sass -I . ./src/scss/main.scss dist/main.css
}
function build_js() {
pnpm exec esbuild --log-level=error --define:process.env.__VERSION__=\"${VERSION}\" --define:process.env.__GIT_HASH__=\"${GIT_HASH}\" --bundle $1 --outdir=dist --target=es6 --loader:.svg=dataurl --format=iife --sourcemap --jsx-factory=h --jsx-fragment=Fragment --platform=browser --minify
}
function build_html() {
cat html/$1.html \
| sed -e '/ANASTASIS_SCRIPT_CONTENT/ {' -e 'r dist/main.js' -e 'd' -e '}' \
| sed -e '/ANASTASIS_STYLE_CONTENT/ {' -e 'r dist/main.css' -e 'd' -e '}' \
>dist/$1.html
}
function cleanup {
trap - SIGHUP SIGINT SIGTERM SIGQUIT
echo -n "Cleaning up... "
wait
kill -- -$$
exit 1
}
trap cleanup SIGHUP SIGINT SIGTERM SIGQUIT
set -e
echo compile
build_css &
build_js src/main.ts &
build_js src/stories.tsx &
build_js src/main.test.ts &
for file in $(find src/ -name test.ts); do build_js $file; done &
wait -n
wait -n
wait -n
wait -n
wait -n
pnpm run --silent test -- -R dot
echo html
build_html ui
build_html ui-dev
build_html stories
if [ "WATCH" == "$1" ]; then
echo watch mode
echo Writing any file in the src directory will trigger a browser reload.
echo Be sure that the watcher server is running.
echo ./watch/serve.sh
inotifywait -e close_write -r src -q -m | while read line; do
echo $(date) $line
build_js src/main.ts
build_html ui-dev
build_js src/stories.tsx
build_html stories
./watch/send.sh '{"type":"RELOAD"}'
done;
fi

View File

@ -14,87 +14,18 @@
You should have received a copy of the GNU Affero General Public License along with 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/> GNU Anastasis; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/ */
/* eslint-disable no-undef */
import esbuild from 'esbuild'
import fs from 'fs';
import WebSocket from "ws";
import chokidar from "chokidar";
const devServerBroadcastDelay = 500 import { serve } from "@gnu-taler/web-util/lib/index.node";
const devServerPort = 8002 import esbuild from 'esbuild';
const wss = new WebSocket.Server({ port: devServerPort }); import { buildConfig } from "./build.mjs";
const toWatch = ["./src"]
function broadcast(file, event) {
setTimeout(() => {
wss.clients.forEach((client) => {
if (client.readyState === WebSocket.OPEN) {
console.log(new Date(), file)
client.send(JSON.stringify(event));
}
});
}, devServerBroadcastDelay);
}
const watcher = chokidar
.watch(toWatch, {
persistent: true,
ignoreInitial: true,
awaitWriteFinish: {
stabilityThreshold: 100,
pollInterval: 100,
},
})
.on("error", (error) => console.error(error))
.on("change", async (file) => {
broadcast(file, { type: "RELOAD" });
})
.on("add", async (file) => {
broadcast(file, { type: "RELOAD" });
})
.on("unlink", async (file) => {
broadcast(file, { type: "RELOAD" });
});
/**
* Just bundling UI Stories.
* FIXME: add linaria CSS after implementing Material so CSS will be bundled
*/
fs.writeFileSync("dist/index.html", fs.readFileSync("html/stories.html"))
fs.writeFileSync("dist/mocha.css", fs.readFileSync("node_modules/mocha/mocha.css"))
fs.writeFileSync("dist/mocha.js", fs.readFileSync("node_modules/mocha/mocha.js"))
fs.writeFileSync("dist/mocha.js.map", fs.readFileSync("node_modules/mocha/mocha.js.map"))
export const buildConfig = {
entryPoints: ['src/main.ts', 'src/stories.tsx'],
bundle: true,
outdir: 'dist',
minify: false,
loader: {
'.svg': 'dataurl',
},
target: [
'es6'
],
format: 'iife',
platform: 'browser',
sourcemap: true,
jsxFactory: 'h',
jsxFragment: 'Fragment',
}
const server = await esbuild
.serve({ servedir: 'dist' }, {
...buildConfig, outdir: 'dist'
})
.catch((e) => {
console.log(e)
process.exit(1)
});
console.log(`Dev server is ready at http://localhost:${server.port}/.
The server is running a using websocket at ${devServerPort} to notify code change and live reload.
`);
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)
})

View File

@ -1,72 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<title>Stories</title>
<style>
/* page css */
div.page {
margin: 0px;
padding: 0px;
font-size: 100%;
font-family: Arial, Helvetica, sans-serif;
}
div.page p:not([class]) {
margin-bottom: 1em;
margin-top: 1em;
}
div.page {
width: 100%;
display: flex;
flex-direction: row;
}
/* sidebar css */
div.sidebar {
min-width: 200px;
height: calc(100vh - 20px);
overflow-y: visible;
overflow-x: hidden;
scroll-behavior: smooth;
}
div.sidebar > ol {
padding: 4px;
}
div.sidebar div:first-child {
background-color: lightcoral;
cursor: pointer;
}
div.sidebar div[data-hide="true"] {
display: none;
}
div.sidebar dd {
margin-left: 1em;
padding: 4px;
cursor: pointer;
border-radius: 4px;
margin-bottom: 4px;
}
div.sidebar dd:nth-child(even) {
background-color: lightgray;
}
div.sidebar dd:nth-child(odd) {
background-color: lightblue;
}
div.sidebar a {
color: black;
}
div.sidebar dd[data-selected] {
background-color: green;
}
/* content css */
div.content {
width: 100%;
padding: 20px;
}
</style>
<script src="./stories.js"></script>
<link rel="stylesheet" href="./main.css" />
</head>
<body>
<taler-stories id="container"></taler-stories>
</body>
</html>

View File

@ -1,65 +0,0 @@
<!DOCTYPE html>
<html
lang="en"
class="has-aside-left has-aside-mobile-transition has-navbar-fixed-top has-aside-expanded"
>
<head>
<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" />
<style id="style-id" type="text/css">
/* <![CDATA[ */
ANASTASIS_STYLE_CONTENT
/* <![CDATA[ */
</style>
</head>
<body>
<div id="container" class="anastasis-container"></div>
<script id="code" type="application/javascript">
ANASTASIS_SCRIPT_CONTENT;
</script>
<script type="application/javascript">
function setupLiveReload() {
const socketPath = `ws://localhost:8003/socket`;
console.log("connecting to ", socketPath);
const ws = new WebSocket(socketPath);
ws.onmessage = (message) => {
const event = JSON.parse(message.data);
if (event.type === "LOG") {
console.log(event.message);
}
if (event.type === "RELOAD") {
window.location.reload();
}
if (event.type === "UPDATE") {
document.body.removeChild(document.getElementById("container"));
const d = document.createElement("div");
d.setAttribute("id", "container");
d.setAttribute("class", "anastasis-container");
document.body.appendChild(d);
const s = document.createElement("script");
s.setAttribute("id", "code");
s.setAttribute("type", "application/javascript");
s.textContent = atob(event.content);
document.body.appendChild(s);
}
};
ws.onerror = (error) => {
console.error(error);
};
ws.onclose = (e) => {
setTimeout(setupLiveReload, 500);
};
}
setupLiveReload();
</script>
</body>
</html>

View File

@ -15,11 +15,12 @@
"dependencies": { "dependencies": {
"@gnu-taler/anastasis-core": "workspace:*", "@gnu-taler/anastasis-core": "workspace:*",
"@gnu-taler/taler-util": "workspace:*", "@gnu-taler/taler-util": "workspace:*",
"@gnu-taler/web-util": "workspace:*",
"@types/chai": "^4.3.0", "@types/chai": "^4.3.0",
"chai": "^4.3.6", "chai": "^4.3.6",
"date-fns": "2.29.2", "date-fns": "2.29.2",
"jed": "1.1.1", "jed": "1.1.1",
"preact": "^10.5.15", "preact": "10.11.3",
"preact-render-to-string": "^5.1.19", "preact-render-to-string": "^5.1.19",
"preact-router": "^3.2.1", "preact-router": "^3.2.1",
"qrcode-generator": "^1.4.4" "qrcode-generator": "^1.4.4"
@ -41,12 +42,9 @@
"bulma": "^0.9.3", "bulma": "^0.9.3",
"bulma-checkbox": "^1.1.1", "bulma-checkbox": "^1.1.1",
"bulma-radio": "^1.1.1", "bulma-radio": "^1.1.1",
"chokidar": "^3.5.3",
"eslint-plugin-header": "^3.1.1",
"jssha": "^3.2.0", "jssha": "^3.2.0",
"mocha": "^9.2.0", "mocha": "^9.2.0",
"sass": "1.32.13", "sass": "1.56.1",
"typescript": "^4.8.4", "typescript": "^4.8.4"
"ws": "7.4.5"
} }
} }

View File

@ -22,7 +22,7 @@
import { BackupStates, RecoveryStates } from "@gnu-taler/anastasis-core"; import { BackupStates, RecoveryStates } from "@gnu-taler/anastasis-core";
import { Fragment, h, VNode } from "preact"; import { Fragment, h, VNode } from "preact";
import { useAnastasisContext } from "../../context/anastasis.js"; import { useAnastasisContext } from "../../context/anastasis.js";
import { Translate } from "../../i18n/index.js"; import { useTranslationContext } from "../../context/translation.js";
interface Props { interface Props {
mobile?: boolean; mobile?: boolean;
@ -34,6 +34,7 @@ const VERSION_WITH_HASH = GIT_HASH ? `${VERSION}-${GIT_HASH}` : VERSION;
export function Sidebar({ mobile }: Props): VNode { export function Sidebar({ mobile }: Props): VNode {
const reducer = useAnastasisContext()!; const reducer = useAnastasisContext()!;
const { i18n } = useTranslationContext();
function saveSession(): void { function saveSession(): void {
const state = reducer.exportState(); const state = reducer.exportState();
@ -64,7 +65,7 @@ export function Sidebar({ mobile }: Props): VNode {
<div class="menu is-menu-main"> <div class="menu is-menu-main">
{!reducer.currentReducerState && ( {!reducer.currentReducerState && (
<p class="menu-label"> <p class="menu-label">
<Translate>Backup or Recorver</Translate> <i18n.Translate>Backup or Recorver</i18n.Translate>
</p> </p>
)} )}
<ul class="menu-list"> <ul class="menu-list">
@ -72,7 +73,7 @@ export function Sidebar({ mobile }: Props): VNode {
<li> <li>
<div class="ml-4"> <div class="ml-4">
<span class="menu-item-label"> <span class="menu-item-label">
<Translate>Select one option</Translate> <i18n.Translate>Select one option</i18n.Translate>
</span> </span>
</div> </div>
</li> </li>
@ -91,7 +92,7 @@ export function Sidebar({ mobile }: Props): VNode {
> >
<div class="ml-4"> <div class="ml-4">
<span class="menu-item-label"> <span class="menu-item-label">
<Translate>Location</Translate> <i18n.Translate>Location</i18n.Translate>
</span> </span>
</div> </div>
</li> </li>
@ -105,7 +106,7 @@ export function Sidebar({ mobile }: Props): VNode {
> >
<div class="ml-4"> <div class="ml-4">
<span class="menu-item-label"> <span class="menu-item-label">
<Translate>Personal information</Translate> <i18n.Translate>Personal information</i18n.Translate>
</span> </span>
</div> </div>
</li> </li>
@ -119,7 +120,7 @@ export function Sidebar({ mobile }: Props): VNode {
> >
<div class="ml-4"> <div class="ml-4">
<span class="menu-item-label"> <span class="menu-item-label">
<Translate>Authorization methods</Translate> <i18n.Translate>Authorization methods</i18n.Translate>
</span> </span>
</div> </div>
</li> </li>
@ -133,7 +134,7 @@ export function Sidebar({ mobile }: Props): VNode {
> >
<div class="ml-4"> <div class="ml-4">
<span class="menu-item-label"> <span class="menu-item-label">
<Translate>Policies</Translate> <i18n.Translate>Policies</i18n.Translate>
</span> </span>
</div> </div>
</li> </li>
@ -147,14 +148,14 @@ export function Sidebar({ mobile }: Props): VNode {
> >
<div class="ml-4"> <div class="ml-4">
<span class="menu-item-label"> <span class="menu-item-label">
<Translate>Secret input</Translate> <i18n.Translate>Secret input</i18n.Translate>
</span> </span>
</div> </div>
</li> </li>
{/* <li class={reducer.currentReducerState.backup_state === BackupStates.PoliciesPaying ? 'is-active' : ''}> {/* <li class={reducer.currentReducerState.backup_state === BackupStates.PoliciesPaying ? 'is-active' : ''}>
<div class="ml-4"> <div class="ml-4">
<span class="menu-item-label"><Translate>Payment (optional)</Translate></span> <span class="menu-item-label"><i18n.Translate>Payment (optional)</i18n.Translate></span>
</div> </div>
</li> */} </li> */}
<li <li
@ -167,14 +168,14 @@ export function Sidebar({ mobile }: Props): VNode {
> >
<div class="ml-4"> <div class="ml-4">
<span class="menu-item-label"> <span class="menu-item-label">
<Translate>Backup completed</Translate> <i18n.Translate>Backup completed</i18n.Translate>
</span> </span>
</div> </div>
</li> </li>
{/* <li class={reducer.currentReducerState.backup_state === BackupStates.TruthsPaying ? 'is-active' : ''}> {/* <li class={reducer.currentReducerState.backup_state === BackupStates.TruthsPaying ? 'is-active' : ''}>
<div class="ml-4"> <div class="ml-4">
<span class="menu-item-label"><Translate>Truth Paying</Translate></span> <span class="menu-item-label"><i18n.Translate>Truth Paying</i18n.Translate></span>
</div> </div>
</li> */} </li> */}
{reducer.currentReducerState.backup_state !== {reducer.currentReducerState.backup_state !==
@ -219,7 +220,7 @@ export function Sidebar({ mobile }: Props): VNode {
> >
<div class="ml-4"> <div class="ml-4">
<span class="menu-item-label"> <span class="menu-item-label">
<Translate>Location</Translate> <i18n.Translate>Location</i18n.Translate>
</span> </span>
</div> </div>
</li> </li>
@ -233,7 +234,7 @@ export function Sidebar({ mobile }: Props): VNode {
> >
<div class="ml-4"> <div class="ml-4">
<span class="menu-item-label"> <span class="menu-item-label">
<Translate>Personal information</Translate> <i18n.Translate>Personal information</i18n.Translate>
</span> </span>
</div> </div>
</li> </li>
@ -247,7 +248,7 @@ export function Sidebar({ mobile }: Props): VNode {
> >
<div class="ml-4"> <div class="ml-4">
<span class="menu-item-label"> <span class="menu-item-label">
<Translate>Secret selection</Translate> <i18n.Translate>Secret selection</i18n.Translate>
</span> </span>
</div> </div>
</li> </li>
@ -263,7 +264,7 @@ export function Sidebar({ mobile }: Props): VNode {
> >
<div class="ml-4"> <div class="ml-4">
<span class="menu-item-label"> <span class="menu-item-label">
<Translate>Solve Challenges</Translate> <i18n.Translate>Solve Challenges</i18n.Translate>
</span> </span>
</div> </div>
</li> </li>
@ -277,7 +278,7 @@ export function Sidebar({ mobile }: Props): VNode {
> >
<div class="ml-4"> <div class="ml-4">
<span class="menu-item-label"> <span class="menu-item-label">
<Translate>Secret recovered</Translate> <i18n.Translate>Secret recovered</i18n.Translate>
</span> </span>
</div> </div>
</li> </li>

View File

@ -21,7 +21,7 @@
import { h, VNode } from "preact"; import { h, VNode } from "preact";
import { useState } from "preact/hooks"; import { useState } from "preact/hooks";
import { useTranslator } from "../../i18n/index.js"; import { useTranslationContext } from "../../context/translation.js";
import "../../scss/DurationPicker.scss"; import "../../scss/DurationPicker.scss";
export interface Props { export interface Props {
@ -46,13 +46,13 @@ export function DurationPicker({
const ms = ss * 60; const ms = ss * 60;
const hs = ms * 60; const hs = ms * 60;
const ds = hs * 24; const ds = hs * 24;
const i18n = useTranslator(); const { i18n } = useTranslationContext();
return ( return (
<div class="rdp-picker"> <div class="rdp-picker">
{days && ( {days && (
<DurationColumn <DurationColumn
unit={i18n`days`} unit={i18n.str`days`}
max={99} max={99}
value={Math.floor(value / ds)} value={Math.floor(value / ds)}
onDecrease={value >= ds ? () => onChange(value - ds) : undefined} onDecrease={value >= ds ? () => onChange(value - ds) : undefined}
@ -62,7 +62,7 @@ export function DurationPicker({
)} )}
{hours && ( {hours && (
<DurationColumn <DurationColumn
unit={i18n`hours`} unit={i18n.str`hours`}
max={23} max={23}
min={1} min={1}
value={Math.floor(value / hs) % 24} value={Math.floor(value / hs) % 24}
@ -73,7 +73,7 @@ export function DurationPicker({
)} )}
{minutes && ( {minutes && (
<DurationColumn <DurationColumn
unit={i18n`minutes`} unit={i18n.str`minutes`}
max={59} max={59}
min={1} min={1}
value={Math.floor(value / ms) % 60} value={Math.floor(value / ms) % 60}
@ -84,7 +84,7 @@ export function DurationPicker({
)} )}
{seconds && ( {seconds && (
<DurationColumn <DurationColumn
unit={i18n`seconds`} unit={i18n.str`seconds`}
max={59} max={59}
value={Math.floor(value / ss) % 60} value={Math.floor(value / ss) % 60}
onDecrease={value >= ss ? () => onChange(value - ss) : undefined} onDecrease={value >= ss ? () => onChange(value - ss) : undefined}

View File

@ -19,23 +19,42 @@
* @author Sebastian Javier Marchano (sebasjm) * @author Sebastian Javier Marchano (sebasjm)
*/ */
import { i18n, setupI18n } from "@gnu-taler/taler-util";
import { createContext, h, VNode } from "preact"; import { createContext, h, VNode } from "preact";
import { useContext, useEffect } from "preact/hooks"; import { useContext, useEffect } from "preact/hooks";
import { useLang } from "../hooks/index.js"; import { useLang } from "../hooks/useLang.js";
import * as jedLib from "jed";
import { strings } from "../i18n/strings.js"; import { strings } from "../i18n/strings.js";
interface Type { interface Type {
lang: string; lang: string;
handler: any; supportedLang: { [id in keyof typeof supportedLang]: string };
changeLanguage: (l: string) => void; 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 = { const initial = {
lang: "en", lang: "en",
handler: null, supportedLang,
changeLanguage: () => { changeLanguage: () => {
// do not change anything // do not change anything
}, },
i18n,
isSaved: false,
}; };
const Context = createContext<Type>(initial); const Context = createContext<Type>(initial);
@ -50,15 +69,23 @@ export const TranslationProvider = ({
children, children,
forceLang, forceLang,
}: Props): VNode => { }: Props): VNode => {
const [lang, changeLanguage] = useLang(initial); const [lang, changeLanguage, isSaved] = useLang(initial);
useEffect(() => { useEffect(() => {
if (forceLang) { if (forceLang) {
changeLanguage(forceLang); changeLanguage(forceLang);
} }
}); });
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, { return h(Context.Provider, {
value: { lang, handler, changeLanguage }, value: { lang, changeLanguage, supportedLang, i18n, isSaved },
children, children,
}); });
}; };

View File

@ -19,7 +19,9 @@
* @author Sebastian Javier Marchano (sebasjm) * @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); export type ValueOrFunction<T> = T | ((p: T) => T);
const calculateRootPath = () => { const calculateRootPath = () => {
@ -69,69 +71,4 @@ export function useBackendInstanceToken(
} }
return [token, setToken]; 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);
return useNotNullLocalStorage("lang-preference", defaultLang);
}
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];
}

View File

@ -13,29 +13,18 @@
You should have received a copy of the GNU Affero General Public License along with 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/> GNU Anastasis; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/ */
import { setupI18n } from "@gnu-taler/taler-util";
import { h, render } from "preact";
import App from "./components/app.js";
function main(): void { import { useNotNullLocalStorage } from "./useLocalStorage.js";
try {
const container = document.getElementById("container"); function getBrowserLang(): string | undefined {
if (!container) { if (window.navigator.languages) return window.navigator.languages[0];
throw Error("container not found, can't mount page contents"); if (window.navigator.language) return window.navigator.language;
} return undefined;
render(h(App, {}), container);
} catch (e) {
console.error("got error", e);
if (e instanceof Error) {
document.body.innerText = `Fatal error: "${e.message}". Please report this bug at https://bugs.gnunet.org/.`;
}
}
} }
// setupI18n("en", strings); export function useLang(
initial?: string,
if (document.readyState === "loading") { ): [string, (s: string) => void, boolean] {
document.addEventListener("DOMContentLoaded", main); const defaultLang = (getBrowserLang() || initial || "en").substring(0, 2);
} else { return useNotNullLocalStorage("lang-preference", defaultLang);
main();
} }

View File

@ -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];
}

View File

@ -1,211 +0,0 @@
/*
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/>
*/
/**
* Translation helpers for React components and template literals.
*/
/**
* Imports
*/
import { ComponentChild, ComponentChildren, h, Fragment, VNode } from "preact";
import { useTranslationContext } from "../context/translation.js";
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>;
}

View File

@ -1,28 +1,42 @@
<!--
This file is part of GNU Taler
(C) 2021--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
-->
<!DOCTYPE html> <!DOCTYPE html>
<html <html
lang="en" lang="en"
class="has-aside-left has-aside-mobile-transition has-navbar-fixed-top has-aside-expanded" class="has-aside-left has-aside-mobile-transition has-navbar-fixed-top has-aside-expanded"
> >
<head> <head>
<meta http-equiv="content-type" content="text/html; charset=utf-8" />
<meta charset="utf-8" /> <meta charset="utf-8" />
<meta name="viewport" content="width=device-width,initial-scale=1" /> <meta name="viewport" content="width=device-width,initial-scale=1" />
<meta name="mobile-web-app-capable" content="yes" /> <meta name="mobile-web-app-capable" content="yes" />
<meta name="apple-mobile-web-app-capable" content="yes" /> <meta name="apple-mobile-web-app-capable" content="yes" />
<link <link
rel="icon" 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==" 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" /> <link rel="shortcut icon" href="data:image/x-icon;," type="image/x-icon" />
<style id="style-id"> <title>Anastasis</title>
ANASTASIS_STYLE_CONTENT <!-- Entry point for the demobank SPA. -->
</style> <script type="module" src="index.js"></script>
<link rel="stylesheet" href="index.css" />
</head> </head>
<body> <body>
<div id="container" class="anastasis-container"></div> <div id="container"></div>
<script>
ANASTASIS_SCRIPT_CONTENT;
</script>
</body> </body>
</html> </html>

View File

@ -13,7 +13,30 @@
You should have received a copy of the GNU Affero General Public License along with 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/> GNU Anastasis; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/ */
import { setupI18n } from "@gnu-taler/taler-util";
import { h, render } from "preact";
import App from "./components/app.js"; import App from "./components/app.js";
import "./scss/main.scss"; import "./scss/main.scss";
export default App; function main(): void {
try {
const container = document.getElementById("container");
if (!container) {
throw Error("container not found, can't mount page contents");
}
render(h(App, {}), container);
} catch (e) {
console.error("got error", e);
if (e instanceof Error) {
document.body.innerText = `Fatal error: "${e.message}". Please report this bug at https://bugs.gnunet.org/.`;
}
}
}
// setupI18n("en", strings);
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", main);
} else {
main();
}

View File

@ -24,6 +24,7 @@ import { createExampleWithoutAnastasis } from "../../../utils/index.jsx";
import { WithoutProviderType, WithProviderType } from "./views.jsx"; import { WithoutProviderType, WithProviderType } from "./views.jsx";
export default { export default {
title: "Adding Provider Screen",
args: { args: {
order: 1, order: 1,
}, },

View File

@ -23,8 +23,10 @@ import { TextInput } from "../../../components/fields/TextInput.js";
import { Notifications } from "../../../components/Notifications.js"; import { Notifications } from "../../../components/Notifications.js";
import { AnastasisClientFrame } from "../index.js"; import { AnastasisClientFrame } from "../index.js";
import { testProvider, WithoutType, WithType } from "./index.js"; import { testProvider, WithoutType, WithType } from "./index.js";
import { useTranslationContext } from "../../../context/translation.js";
export function WithProviderType(props: WithType): VNode { export function WithProviderType(props: WithType): VNode {
const { i18n } = useTranslationContext();
return ( return (
<AnastasisClientFrame <AnastasisClientFrame
hideNav hideNav
@ -33,7 +35,7 @@ export function WithProviderType(props: WithType): VNode {
> >
<div> <div>
<Notifications notifications={props.notifications} /> <Notifications notifications={props.notifications} />
<p>Add a provider url for a {props.providerLabel} service</p> <p>{i18n.str`Add a provider url for a ${props.providerLabel} service`}</p>
<div class="container"> <div class="container">
<TextInput <TextInput
label="Provider URL" label="Provider URL"

View File

@ -24,6 +24,7 @@ import { createExample, reducerStatesExample } from "../../utils/index.js";
import { AttributeEntryScreen as TestedComponent } from "./AttributeEntryScreen.js"; import { AttributeEntryScreen as TestedComponent } from "./AttributeEntryScreen.js";
export default { export default {
title: "Attribute Entry Screen",
component: TestedComponent, component: TestedComponent,
args: { args: {
order: 3, order: 3,

View File

@ -24,6 +24,7 @@ import { createExample, reducerStatesExample } from "../../utils/index.js";
import { AuthenticationEditorScreen as TestedComponent } from "./AuthenticationEditorScreen.js"; import { AuthenticationEditorScreen as TestedComponent } from "./AuthenticationEditorScreen.js";
export default { export default {
title: "Authentication Editor Screen",
component: TestedComponent, component: TestedComponent,
args: { args: {
order: 4, order: 4,

View File

@ -24,6 +24,7 @@ import { createExample, reducerStatesExample } from "../../utils/index.js";
import { BackupFinishedScreen as TestedComponent } from "./BackupFinishedScreen.js"; import { BackupFinishedScreen as TestedComponent } from "./BackupFinishedScreen.js";
export default { export default {
title: "Backup finish",
component: TestedComponent, component: TestedComponent,
args: { args: {
order: 8, order: 8,

View File

@ -28,6 +28,7 @@ import { createExample, reducerStatesExample } from "../../utils/index.js";
import { ChallengeOverviewScreen as TestedComponent } from "./ChallengeOverviewScreen.js"; import { ChallengeOverviewScreen as TestedComponent } from "./ChallengeOverviewScreen.js";
export default { export default {
title: "Challenge overview",
component: TestedComponent, component: TestedComponent,
args: { args: {
order: 5, order: 5,

View File

@ -23,6 +23,7 @@ import { createExample, reducerStatesExample } from "../../utils/index.js";
import { ChallengePayingScreen as TestedComponent } from "./ChallengePayingScreen.js"; import { ChallengePayingScreen as TestedComponent } from "./ChallengePayingScreen.js";
export default { export default {
title: "Challenge paying",
component: TestedComponent, component: TestedComponent,
args: { args: {
order: 10, order: 10,

View File

@ -24,6 +24,7 @@ import { createExample, reducerStatesExample } from "../../utils/index.js";
import { ContinentSelectionScreen as TestedComponent } from "./ContinentSelectionScreen.js"; import { ContinentSelectionScreen as TestedComponent } from "./ContinentSelectionScreen.js";
export default { export default {
title: "Continent selection",
component: TestedComponent, component: TestedComponent,
args: { args: {
order: 2, order: 2,

View File

@ -24,6 +24,7 @@ import { createExample, reducerStatesExample } from "../../utils/index.js";
import { EditPoliciesScreen as TestedComponent } from "./EditPoliciesScreen.js"; import { EditPoliciesScreen as TestedComponent } from "./EditPoliciesScreen.js";
export default { export default {
title: "Edit policies",
args: { args: {
order: 6, order: 6,
}, },

View File

@ -24,6 +24,7 @@ import { createExample, reducerStatesExample } from "../../utils/index.js";
import { PoliciesPayingScreen as TestedComponent } from "./PoliciesPayingScreen.js"; import { PoliciesPayingScreen as TestedComponent } from "./PoliciesPayingScreen.js";
export default { export default {
title: "Policies paying",
component: TestedComponent, component: TestedComponent,
args: { args: {
order: 9, order: 9,

View File

@ -25,6 +25,7 @@ import { createExample, reducerStatesExample } from "../../utils/index.js";
import { RecoveryFinishedScreen as TestedComponent } from "./RecoveryFinishedScreen.js"; import { RecoveryFinishedScreen as TestedComponent } from "./RecoveryFinishedScreen.js";
export default { export default {
title: "Recovery Finished",
args: { args: {
order: 7, order: 7,
}, },

View File

@ -24,6 +24,7 @@ import { createExample, reducerStatesExample } from "../../utils/index.js";
import { ReviewPoliciesScreen as TestedComponent } from "./ReviewPoliciesScreen.js"; import { ReviewPoliciesScreen as TestedComponent } from "./ReviewPoliciesScreen.js";
export default { export default {
title: "Reviewing Policies",
args: { args: {
order: 6, order: 6,
}, },

View File

@ -24,6 +24,7 @@ import { createExample, reducerStatesExample } from "../../utils/index.js";
import { SecretEditorScreen as TestedComponent } from "./SecretEditorScreen.js"; import { SecretEditorScreen as TestedComponent } from "./SecretEditorScreen.js";
export default { export default {
title: "Secret editor",
component: TestedComponent, component: TestedComponent,
args: { args: {
order: 7, order: 7,

View File

@ -27,6 +27,7 @@ import {
} from "./SecretSelectionScreen.js"; } from "./SecretSelectionScreen.js";
export default { export default {
title: "Secret selection",
component: SecretSelectionScreen, component: SecretSelectionScreen,
args: { args: {
order: 4, order: 4,

View File

@ -24,6 +24,7 @@ import { createExample, reducerStatesExample } from "../../utils/index.js";
import { SolveScreen as TestedComponent } from "./SolveScreen.js"; import { SolveScreen as TestedComponent } from "./SolveScreen.js";
export default { export default {
title: "Solve Screen",
component: TestedComponent, component: TestedComponent,
args: { args: {
order: 6, order: 6,

View File

@ -23,6 +23,7 @@ import { createExample, reducerStatesExample } from "../../utils/index.js";
import { StartScreen as TestedComponent } from "./StartScreen.js"; import { StartScreen as TestedComponent } from "./StartScreen.js";
export default { export default {
title: "Start screen",
component: TestedComponent, component: TestedComponent,
args: { args: {
order: 1, order: 1,

View File

@ -24,6 +24,7 @@ import { createExample, reducerStatesExample } from "../../utils/index.js";
import { TruthsPayingScreen as TestedComponent } from "./TruthsPayingScreen.js"; import { TruthsPayingScreen as TestedComponent } from "./TruthsPayingScreen.js";
export default { export default {
title: "Truths Paying",
component: TestedComponent, component: TestedComponent,
args: { args: {
order: 10, order: 10,

View File

@ -23,6 +23,7 @@ import { createExample, reducerStatesExample } from "../../../utils/index.js";
import { authMethods as TestedComponent, KnownAuthMethods } from "./index.js"; import { authMethods as TestedComponent, KnownAuthMethods } from "./index.js";
export default { export default {
title: "Auth method: Email setup",
component: TestedComponent, component: TestedComponent,
args: { args: {
order: 5, order: 5,

View File

@ -27,6 +27,7 @@ import { createExample, reducerStatesExample } from "../../../utils/index.js";
import { authMethods as TestedComponent, KnownAuthMethods } from "./index.js"; import { authMethods as TestedComponent, KnownAuthMethods } from "./index.js";
export default { export default {
title: "Auth method: Email solve",
component: TestedComponent, component: TestedComponent,
args: { args: {
order: 5, order: 5,

View File

@ -23,7 +23,7 @@ import { useState } from "preact/hooks";
import { AsyncButton } from "../../../components/AsyncButton.js"; import { AsyncButton } from "../../../components/AsyncButton.js";
import { TextInput } from "../../../components/fields/TextInput.js"; import { TextInput } from "../../../components/fields/TextInput.js";
import { useAnastasisContext } from "../../../context/anastasis.js"; import { useAnastasisContext } from "../../../context/anastasis.js";
import { useTranslator } from "../../../i18n/index.js"; import { useTranslationContext } from "../../../context/translation.js";
import { AnastasisClientFrame } from "../index.js"; import { AnastasisClientFrame } from "../index.js";
import { SolveOverviewFeedbackDisplay } from "../SolveScreen.js"; import { SolveOverviewFeedbackDisplay } from "../SolveScreen.js";
import { shouldHideConfirm } from "./helpers.js"; import { shouldHideConfirm } from "./helpers.js";
@ -53,7 +53,7 @@ export function AuthMethodEmailSolve({ id }: AuthMethodSolveProps): VNode {
_setAnswer(result); _setAnswer(result);
} }
const [expanded, setExpanded] = useState(false); const [expanded, setExpanded] = useState(false);
const i18n = useTranslator(); const { i18n } = useTranslationContext();
const reducer = useAnastasisContext(); const reducer = useAnastasisContext();
if (!reducer) { if (!reducer) {
@ -124,7 +124,7 @@ export function AuthMethodEmailSolve({ id }: AuthMethodSolveProps): VNode {
const error = const error =
answer.length > 21 answer.length > 21
? i18n`The answer should not be greater than 21 characters.` ? i18n.str`The answer should not be greater than 21 characters.`
: undefined; : undefined;
return ( return (

View File

@ -23,6 +23,7 @@ import { createExample, reducerStatesExample } from "../../../utils/index.js";
import { authMethods as TestedComponent, KnownAuthMethods } from "./index.js"; import { authMethods as TestedComponent, KnownAuthMethods } from "./index.js";
export default { export default {
title: "Auth method: IBAN setup",
component: TestedComponent, component: TestedComponent,
args: { args: {
order: 5, order: 5,

View File

@ -24,6 +24,7 @@ import { createExample, reducerStatesExample } from "../../../utils/index.js";
import { authMethods as TestedComponent, KnownAuthMethods } from "./index.js"; import { authMethods as TestedComponent, KnownAuthMethods } from "./index.js";
export default { export default {
title: "Auth method: IBAN Solve",
component: TestedComponent, component: TestedComponent,
args: { args: {
order: 5, order: 5,

View File

@ -23,6 +23,7 @@ import { createExample, reducerStatesExample } from "../../../utils/index.js";
import { authMethods as TestedComponent, KnownAuthMethods } from "./index.js"; import { authMethods as TestedComponent, KnownAuthMethods } from "./index.js";
export default { export default {
title: "Auth method: Post setup",
component: TestedComponent, component: TestedComponent,
args: { args: {
order: 5, order: 5,

View File

@ -24,6 +24,7 @@ import { createExample, reducerStatesExample } from "../../../utils/index.js";
import { authMethods as TestedComponent, KnownAuthMethods } from "./index.js"; import { authMethods as TestedComponent, KnownAuthMethods } from "./index.js";
export default { export default {
title: "Auth method: Post solve",
component: TestedComponent, component: TestedComponent,
args: { args: {
order: 5, order: 5,

View File

@ -19,7 +19,7 @@ import { useState } from "preact/hooks";
import { AsyncButton } from "../../../components/AsyncButton.js"; import { AsyncButton } from "../../../components/AsyncButton.js";
import { TextInput } from "../../../components/fields/TextInput.js"; import { TextInput } from "../../../components/fields/TextInput.js";
import { useAnastasisContext } from "../../../context/anastasis.js"; import { useAnastasisContext } from "../../../context/anastasis.js";
import { useTranslator } from "../../../i18n/index.js"; import { useTranslationContext } from "../../../context/translation.js";
import { AnastasisClientFrame } from "../index.js"; import { AnastasisClientFrame } from "../index.js";
import { SolveOverviewFeedbackDisplay } from "../SolveScreen.js"; import { SolveOverviewFeedbackDisplay } from "../SolveScreen.js";
import { shouldHideConfirm } from "./helpers.js"; import { shouldHideConfirm } from "./helpers.js";
@ -48,7 +48,7 @@ export function AuthMethodPostSolve({ id }: AuthMethodSolveProps): VNode {
_setAnswer(result); _setAnswer(result);
} }
const i18n = useTranslator(); const { i18n } = useTranslationContext();
const reducer = useAnastasisContext(); const reducer = useAnastasisContext();
if (!reducer) { if (!reducer) {
@ -119,7 +119,7 @@ export function AuthMethodPostSolve({ id }: AuthMethodSolveProps): VNode {
const error = const error =
answer.length > 21 answer.length > 21
? i18n`The answer should not be greater than 21 characters.` ? i18n.str`The answer should not be greater than 21 characters.`
: undefined; : undefined;
return ( return (

View File

@ -23,6 +23,7 @@ import { createExample, reducerStatesExample } from "../../../utils/index.js";
import { authMethods as TestedComponent, KnownAuthMethods } from "./index.js"; import { authMethods as TestedComponent, KnownAuthMethods } from "./index.js";
export default { export default {
title: "Auth method: Question setup",
component: TestedComponent, component: TestedComponent,
args: { args: {
order: 5, order: 5,

View File

@ -27,6 +27,7 @@ import { createExample, reducerStatesExample } from "../../../utils/index.js";
import { authMethods as TestedComponent, KnownAuthMethods } from "./index.js"; import { authMethods as TestedComponent, KnownAuthMethods } from "./index.js";
export default { export default {
title: "Auth method: Question solve",
component: TestedComponent, component: TestedComponent,
args: { args: {
order: 5, order: 5,

View File

@ -23,6 +23,7 @@ import { createExample, reducerStatesExample } from "../../../utils/index.js";
import { authMethods as TestedComponent, KnownAuthMethods } from "./index.js"; import { authMethods as TestedComponent, KnownAuthMethods } from "./index.js";
export default { export default {
title: "Auth method: SMS setup",
component: TestedComponent, component: TestedComponent,
args: { args: {
order: 5, order: 5,

View File

@ -24,6 +24,7 @@ import { createExample, reducerStatesExample } from "../../../utils/index.js";
import { authMethods as TestedComponent, KnownAuthMethods } from "./index.js"; import { authMethods as TestedComponent, KnownAuthMethods } from "./index.js";
export default { export default {
title: "Auth method: SMS solve",
component: TestedComponent, component: TestedComponent,
args: { args: {
order: 5, order: 5,

View File

@ -19,7 +19,7 @@ import { useState } from "preact/hooks";
import { AsyncButton } from "../../../components/AsyncButton.js"; import { AsyncButton } from "../../../components/AsyncButton.js";
import { TextInput } from "../../../components/fields/TextInput.js"; import { TextInput } from "../../../components/fields/TextInput.js";
import { useAnastasisContext } from "../../../context/anastasis.js"; import { useAnastasisContext } from "../../../context/anastasis.js";
import { useTranslator } from "../../../i18n/index.js"; import { useTranslationContext } from "../../../context/translation.js";
import { AnastasisClientFrame } from "../index.js"; import { AnastasisClientFrame } from "../index.js";
import { SolveOverviewFeedbackDisplay } from "../SolveScreen.js"; import { SolveOverviewFeedbackDisplay } from "../SolveScreen.js";
import { shouldHideConfirm } from "./helpers.js"; import { shouldHideConfirm } from "./helpers.js";
@ -48,7 +48,7 @@ export function AuthMethodSmsSolve({ id }: AuthMethodSolveProps): VNode {
_setAnswer(result); _setAnswer(result);
} }
const i18n = useTranslator(); const { i18n } = useTranslationContext();
const [expanded, setExpanded] = useState(false); const [expanded, setExpanded] = useState(false);
const reducer = useAnastasisContext(); const reducer = useAnastasisContext();
@ -120,7 +120,7 @@ export function AuthMethodSmsSolve({ id }: AuthMethodSolveProps): VNode {
const error = const error =
answer.length > 21 answer.length > 21
? i18n`The answer should not be greater than 21 characters.` ? i18n.str`The answer should not be greater than 21 characters.`
: undefined; : undefined;
return ( return (

View File

@ -23,6 +23,7 @@ import { createExample, reducerStatesExample } from "../../../utils/index.js";
import { authMethods as TestedComponent, KnownAuthMethods } from "./index.js"; import { authMethods as TestedComponent, KnownAuthMethods } from "./index.js";
export default { export default {
title: "Auth method: Totp setup",
component: TestedComponent, component: TestedComponent,
args: { args: {
order: 5, order: 5,

View File

@ -24,6 +24,7 @@ import { createExample, reducerStatesExample } from "../../../utils/index.js";
import { authMethods as TestedComponent, KnownAuthMethods } from "./index.js"; import { authMethods as TestedComponent, KnownAuthMethods } from "./index.js";
export default { export default {
title: "Auth method: Totp solve",
component: TestedComponent, component: TestedComponent,
args: { args: {
order: 5, order: 5,

View File

@ -28,7 +28,7 @@
width: $icon-base-width; width: $icon-base-width;
&.has-update-mark:after { &.has-update-mark:after {
right: ($icon-base-width / 2) - 0.85; right: calc($icon-base-width / 2) - 0.85;
} }
} }
} }

View File

@ -18,302 +18,24 @@
* *
* @author Sebastian Javier Marchano (sebasjm) * @author Sebastian Javier Marchano (sebasjm)
*/ */
import { setupI18n } from "@gnu-taler/taler-util";
import { ComponentChild, Fragment, h, render, VNode } from "preact";
import { useEffect, useErrorBoundary, useState } from "preact/hooks";
import { strings } from "./i18n/strings.js"; import { strings } from "./i18n/strings.js";
import * as pages from "./pages/home/index.storiesNo.js"; import * as pages from "./pages/home/index.storiesNo.js";
const url = new URL(window.location.href); import { renderStories } from "@gnu-taler/web-util/lib/index.browser";
const lang = url.searchParams.get("lang") || "en";
setupI18n(lang, strings); import "./scss/main.scss";
const Page = ({ children }: any) => <div class="page">{children}</div>;
const SideBar = ({ children }: any) => <div class="sidebar">{children}</div>;
const Content = ({ children }: any) => <div class="content">{children}</div>;
function parseExampleImport(
group: string,
im: any,
name?: string,
): ComponentItem {
const component = name || im.default.title;
const order: number = im.default.args?.order || 0;
return {
name: component,
order,
examples: Object.entries(im)
.filter(([k]) => k !== "default")
.map(
([name, render]) =>
({
group,
component,
name,
render,
} as ExampleItem),
),
};
}
function SortStories(a: any, b: any): number { function SortStories(a: any, b: any): number {
return (a?.order ?? 0) - (b?.order ?? 0); return (a?.order ?? 0) - (b?.order ?? 0);
} }
const allExamples = Object.entries({ pages }).map(([title, value]) => { function main(): void {
return { renderStories(
title, { pages },
list: Object.entries(value) {
.filter(([name]) => name != "default") strings,
.map(([name, value]) => parseExampleImport(title, value, name)) },
.sort(SortStories),
};
});
interface ComponentItem {
name: string;
order: number;
examples: ExampleItem[];
}
interface ExampleItem {
group: string;
component: string;
name: string;
render: {
(args: any): VNode;
args: any;
};
}
function findByGroupComponentName(
group: string,
component: string,
name: string,
): ExampleItem | undefined {
const gl = allExamples.filter((e) => e.title === group);
if (gl.length === 0) {
return undefined;
}
const cl = gl[0].list.filter((l) => l.name === component);
if (cl.length === 0) {
return undefined;
}
const el = cl[0].examples.filter((c) => c.name === name);
if (el.length === 0) {
return undefined;
}
return el[0];
}
function getContentForExample(item: ExampleItem | undefined): () => VNode {
if (!item)
return function SelectExampleMessage() {
return <div>select example from the list on the left</div>;
};
const example = findByGroupComponentName(
item.group,
item.component,
item.name,
);
if (!example)
return function ExampleNotFoundMessage() {
return <div>example not found</div>;
};
return () => example.render(example.render.args);
}
function ExampleList({
name,
list,
selected,
onSelectStory,
}: {
name: string;
list: {
name: string;
examples: ExampleItem[];
}[];
selected: ExampleItem | undefined;
onSelectStory: (i: ExampleItem, id: string) => void;
}): VNode {
const [isOpen, setOpen] = useState(selected && selected.group === name);
return (
<ol>
<div onClick={() => setOpen(!isOpen)}>{name}</div>
<div data-hide={!isOpen}>
{list.map((k) => (
<li key={k.name}>
<dl>
<dt>{k.name}</dt>
{k.examples.map((r) => {
const e = encodeURIComponent;
const eId = `${e(r.group)}-${e(r.component)}-${e(r.name)}`;
function doSelection(e: any): void {
e.preventDefault();
location.hash = `#${eId}`;
onSelectStory(r, eId);
}
const isSelected =
selected &&
selected.component === r.component &&
selected.group === r.group &&
selected.name === r.name;
return (
<dd
id={eId}
key={r.name}
data-selected={isSelected}
onClick={doSelection}
>
<a href={`#${eId}`} onClick={doSelection}>
{r.name}
</a>
</dd>
);
})}
</dl>
</li>
))}
</div>
</ol>
);
}
// function getWrapperForGroup(group: string): FunctionComponent {
// switch (group) {
// case "popup":
// return function PopupWrapper({ children }: any) {
// return (
// <Fragment>
// <PopupNavBar />
// <PopupBox>{children}</PopupBox>
// </Fragment>
// );
// };
// case "wallet":
// return function WalletWrapper({ children }: any) {
// return (
// <Fragment>
// <LogoHeader />
// <WalletNavBar />
// <WalletBox>{children}</WalletBox>
// </Fragment>
// );
// };
// case "cta":
// return function WalletWrapper({ children }: any) {
// return (
// <Fragment>
// <WalletBox>{children}</WalletBox>
// </Fragment>
// );
// };
// default:
// return Fragment;
// }
// }
function ErrorReport({
children,
selected,
}: {
children: ComponentChild;
selected: ExampleItem | undefined;
}): VNode {
const [error] = useErrorBoundary();
if (error) {
return (
<div class="error_report">
<p>Error was thrown trying to render</p>
{selected && (
<ul>
<li>
<b>group</b>: {selected.group}
</li>
<li>
<b>component</b>: {selected.component}
</li>
<li>
<b>example</b>: {selected.name}
</li>
<li>
<b>args</b>:{" "}
<pre>{JSON.stringify(selected.render.args, undefined, 2)}</pre>
</li>
</ul>
)}
<p>{error.message}</p>
<pre>{error.stack}</pre>
</div>
);
}
return <Fragment>{children}</Fragment>;
}
function getSelectionFromLocationHash(hash: string): ExampleItem | undefined {
if (!hash) return undefined;
const parts = hash.substring(1).split("-");
if (parts.length < 3) return undefined;
return findByGroupComponentName(
decodeURIComponent(parts[0]),
decodeURIComponent(parts[1]),
decodeURIComponent(parts[2]),
);
}
function Application(): VNode {
const initialSelection = getSelectionFromLocationHash(location.hash);
const [selected, updateSelected] = useState<ExampleItem | undefined>(
initialSelection,
);
useEffect(() => {
if (location.hash) {
const hash = location.hash.substring(1);
const found = document.getElementById(hash);
if (found) {
setTimeout(() => {
found.scrollIntoView({
block: "center",
});
}, 10);
}
}
}, []);
const ExampleContent = getContentForExample(selected);
// const GroupWrapper = getWrapperForGroup(selected?.group || "default");
return (
<Page>
<LiveReload />
<SideBar>
{allExamples.map((e) => (
<ExampleList
key={e.title}
name={e.title}
list={e.list}
selected={selected}
onSelectStory={(item, htmlId) => {
document.getElementById(htmlId)?.scrollIntoView({
block: "center",
});
updateSelected(item);
}}
/>
))}
<hr />
</SideBar>
<Content>
<ErrorReport selected={selected}>
{/* <GroupWrapper> */}
<ExampleContent />
{/* </GroupWrapper> */}
</ErrorReport>
</Content>
</Page>
); );
} }
@ -322,72 +44,3 @@ if (document.readyState === "loading") {
} else { } else {
main(); main();
} }
function main(): void {
try {
const container = document.getElementById("container");
if (!container) {
throw Error("container not found, can't mount page contents");
}
render(<Application />, container);
} catch (e) {
console.error("got error", e);
if (e instanceof Error) {
document.body.innerText = `Fatal error: "${e.message}". Please report this bug at https://bugs.gnunet.org/.`;
}
}
}
let liveReloadMounted = false;
function LiveReload({ port = 8002 }: { port?: number }): VNode {
const [isReloading, setIsReloading] = useState(false);
useEffect(() => {
if (!liveReloadMounted) {
setupLiveReload(port, () => {
setIsReloading(true);
window.location.reload();
});
liveReloadMounted = true;
}
});
if (isReloading) {
return (
<div
style={{
position: "absolute",
width: "100%",
height: "100%",
backgroundColor: "rgba(0,0,0,0.5)",
color: "white",
display: "flex",
justifyContent: "center",
}}
>
<h1 style={{ margin: "auto" }}>reloading...</h1>
</div>
);
}
return <Fragment />;
}
function setupLiveReload(port: number, onReload: () => void): void {
const socketPath = `ws://localhost:8003/socket`;
// const socketPath = `${protocol}//${host}:${port}/socket`;
const ws = new WebSocket(socketPath);
ws.onmessage = (message) => {
const event = JSON.parse(message.data);
if (event.type === "LOG") {
console.log(event.message);
}
if (event.type === "RELOAD") {
onReload();
}
};
ws.onerror = (error) => {
console.error(error);
};
ws.onclose = (e) => {
console.log("disconnected", e);
};
}

View File

@ -41,8 +41,10 @@ export function createExample<Props>(
// check how we can build evaluatedProps in render time // check how we can build evaluatedProps in render time
const evaluatedProps = typeof props === "function" ? props() : props; const evaluatedProps = typeof props === "function" ? props() : props;
const Render = (args: any): VNode => create(Component, args); const Render = (args: any): VNode => create(Component, args);
Render.args = evaluatedProps; return {
return Render; component: Render,
props: evaluatedProps
};
} }
export function createExampleWithCustomContext<Props, ContextProps>( export function createExampleWithCustomContext<Props, ContextProps>(
@ -58,8 +60,10 @@ export function createExampleWithCustomContext<Props, ContextProps>(
...contextProps, ...contextProps,
children: [Render(args)], children: [Render(args)],
} as any); } as any);
WithContext.args = evaluatedProps; return {
return WithContext; component: WithContext,
props: evaluatedProps
};
} }
export function NullLink({ export function NullLink({

View File

@ -37,16 +37,18 @@ export function createExampleWithoutAnastasis<Props>(
// check how we can build evaluatedProps in render time // check how we can build evaluatedProps in render time
const evaluatedProps = typeof props === "function" ? props() : props; const evaluatedProps = typeof props === "function" ? props() : props;
const Render = (args: any): VNode => h(Component, args); const Render = (args: any): VNode => h(Component, args);
Render.args = evaluatedProps; return {
return Render; component: Render,
props: evaluatedProps,
};
} }
export function createExample<Props>( export function createExample<Props>(
Component: FunctionalComponent<Props>, Component: FunctionalComponent<Props>,
currentReducerState?: ReducerState, currentReducerState?: ReducerState,
props?: Partial<Props>, props?: Partial<Props>,
): { (args: Props): VNode } { ): ComponentChildren {
const r = (args: Props): VNode => { const Render = (args: Props): VNode => {
return ( return (
<AnastasisProvider <AnastasisProvider
value={{ value={{
@ -74,8 +76,10 @@ export function createExample<Props>(
</AnastasisProvider> </AnastasisProvider>
); );
}; };
r.args = props; return {
return r; component: Render,
props: props,
};
} }
const base = { const base = {

View File

@ -1,18 +0,0 @@
#!/bin/bash
SERVER_KEY=258EAFA5-E914-47DA-95CA-C5AB0DC85B11
while read line; do
LINE=$(echo $line | tr -d '\r')
case $LINE in
Sec-WebSocket-Key:*)
CLIENT_KEY="${LINE:19}"
export WS_ACCEPT=$( echo -n $CLIENT_KEY$SERVER_KEY | sha1sum | xxd -r -p | base64 )
;;
"") break ;;
esac
done
cat watch/web_socket_server.reply | sed 's/$'"/`echo \\\r`/" | envsubst '$WS_ACCEPT'
tail -n 0 -F /tmp/send_signal 2> /dev/null

View File

@ -1,12 +0,0 @@
#!/bin/bash
#https://datatracker.ietf.org/doc/html/rfc6455#page-65
COMMAND=$1
LEN=$(printf '%x\n' ${#COMMAND})
#text command
OPCODE=81
cat <(echo -n $OPCODE$LEN | xxd -r -p) <(echo -n $COMMAND) >> /tmp/send_signal

View File

@ -1,14 +0,0 @@
#!/bin/bash
#https://datatracker.ietf.org/doc/html/rfc6455#page-65
CONTENT=$( cat $1 | base64 -w 0 )
COMMAND='{"type":"UPDATE","'$CONTENT'"}'
LEN=$(printf '%0*x\n' 4 ${#COMMAND})
echo $LEN
LEN=00000138
#text command
OPCODE=81
cat <(echo -n $OPCODE$LEN | xxd -r -p) <(echo -n $COMMAND) >> /tmp/send_signal

View File

@ -1,7 +0,0 @@
#!/bin/bash
#clean up
rm /tmp/send_signal
socat TCP-LISTEN:8003,fork,reuseaddr,keepalive EXEC:"./watch/reply.sh"

View File

@ -1,6 +0,0 @@
GET /socket HTTP/1.1
Connection: Upgrade
Upgrade: websocket
Sec-WebSocket-Version: 13
Sec-WebSocket-Key: aaaaaaaaaaaaaaaaaaaaaa==

View File

@ -1,5 +0,0 @@
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: $WS_ACCEPT