add template from merchant backoffice

This commit is contained in:
Sebastian 2021-10-19 10:56:52 -03:00
parent 269022a526
commit 5883d42d80
No known key found for this signature in database
GPG Key ID: BE4FF68352439FC1
75 changed files with 3917 additions and 1048 deletions

View File

@ -23,15 +23,20 @@
"dependencies": {
"@gnu-taler/taler-util": "workspace:^0.8.3",
"anastasis-core": "workspace:^0.0.1",
"jed": "1.1.1",
"preact": "^10.3.1",
"preact-render-to-string": "^5.1.4",
"preact-router": "^3.2.1"
},
"devDependencies": {
"@creativebulma/bulma-tooltip": "^1.2.0",
"@types/enzyme": "^3.10.5",
"@types/jest": "^26.0.8",
"@typescript-eslint/eslint-plugin": "^2.25.0",
"@typescript-eslint/parser": "^2.25.0",
"bulma": "^0.9.3",
"bulma-checkbox": "^1.1.1",
"bulma-radio": "^1.1.1",
"enzyme": "^3.11.0",
"enzyme-adapter-preact-pure": "^3.1.0",
"eslint": "^6.8.0",
@ -39,6 +44,8 @@
"jest": "^26.2.2",
"jest-preset-preact": "^4.0.2",
"preact-cli": "^3.2.2",
"sass": "^1.32.13",
"sass-loader": "^10.1.1",
"sirv-cli": "^1.0.0-next.3",
"typescript": "^3.7.5"
},
@ -49,4 +56,4 @@
"<rootDir>/tests/__mocks__/setupTests.ts"
]
}
}
}

View File

@ -0,0 +1,48 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 19.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 2411.2 2794" style="enable-background:new 0 0 2411.2 2794;" xml:space="preserve">
<style type="text/css">
.st0{fill:#FFFFFF;}
.st1{fill-rule:evenodd;clip-rule:evenodd;}
.st2{fill-rule:evenodd;clip-rule:evenodd;fill:#FFFFFF;}
</style>
<g id="Layer_2">
</g>
<g id="Layer_x5F_1_x5F_1">
<g>
<polygon points="1204.6,359.2 271.8,30 271.8,2060.1 1204.6,1758.3 "/>
<polygon class="st0" points="1182.2,358.1 2150.6,29 2150.6,2059 1182.2,1757.3 "/>
<polygon class="st0" points="30,2415.4 1182.2,2031.4 1182.2,357.9 30,742 "/>
<polygon points="1707.2,2440.7 1870.5,2709.4 1956.6,2459.8 "/>
<g>
<path d="M421.7,934.8c-6.1-6,8,49.1,27.6,68.9c34.8,35.1,61.9,39.6,76.4,40.2c32,1.3,71.5-8,94.9-17.8
c22.7-9.7,62.4-30,77.5-59.6c3.2-6.3,11.9-17,6.4-43.2c-4.2-20.2-17-27.3-32.7-26.2c-15.7,1.1-63.2,13.7-86.1,20.8
c-23,7-70.3,21.4-90.9,25.8C474.3,948.2,429,941.7,421.7,934.8z"/>
<path d="M1003.1,1593.7c-9.1-3.3-196.9-81.1-223.6-93.9c-21.8-10.5-75.2-33.1-100.4-43.3c70.8-109.2,115.5-191.6,121.5-204.1
c11-23,86-169.6,87.7-178.7c1.7-9.1,3.8-42.9,2.2-51c-1.7-8.2-29.1,7.6-66.4,20.2c-37.4,12.6-108.4,58.8-135.8,64.6
c-27.5,5.7-115.5,39.1-160.5,54c-45,14.9-130.2,40.9-165.2,50.4c-35.1,9.5-65.7,10.2-85.3,16.2c0,0,2.6,27.5,7.8,35.7
c5.2,8.2,23.7,28.4,45.3,34.1c21.6,5.7,57.3,3.4,73.6-0.3c16.3-3.8,44.4-17.5,48.2-23.6c3.8-6.1-2-24.9,4.5-30.6
c6.5-5.6,92.2-25.7,124.6-35.4c32.4-10,156.3-52.6,173.1-50.5c-5.3,17.7-105,215.1-137.1,274c-32.1,58.9-218.6,318-258.3,363.6
c-30.1,34.7-103.2,123.5-128.5,143.6c6.4,1.8,51.6-2.1,59.9-7.2c51.3-31.6,136.9-138.1,164.4-170.5
c81.9-96,153.8-196.8,210.8-283.4h0.1c11.1,4.6,100.9,77.8,124.4,94c23.4,16.2,115.9,67.8,136,76.4c20,8.7,97.1,44.2,100.3,32.2
C1029.4,1668,1012.2,1597.1,1003.1,1593.7z"/>
</g>
<path class="st1" d="M569,2572c18,11,35,20,54,29c38,19,81,39,122,54c56,21,112,38,168,51c31,7,65,13,98,18c3,0,92,11,110,11h90
c35-3,68-5,103-10c28-4,59-9,89-16c22-5,45-10,67-17c21-6,45-14,68-22c15-5,31-12,47-18c13-6,29-13,44-19c18-8,39-19,59-29
c16-8,34-18,51-28c13-7,43-30,59-30c18,0,30,16,30,30c0,29-39,38-57,51c-19,13-42,23-62,34c-40,21-81,39-120,54
c-51,19-107,37-157,49c-19,4-38,9-57,12c-10,2-114,18-143,18h-132c-35-3-72-7-107-12c-31-5-64-11-95-18c-24-5-50-12-73-19
c-40-11-79-25-117-40c-69-26-141-60-209-105c-12-8-13-16-13-25c0-15,11-29,29-29C531,2546,563,2569,569,2572z"/>
<path class="st1" d="M1151,2009L61,2372V764l1090-363V2009z M1212,354v1680c-1,5-3,10-7,15c-2,3-6,7-9,8c-25,10-1151,388-1166,388
c-12,0-23-8-29-21c0-1-1-2-1-4V739c2-5,3-12,7-16c8-11,22-13,31-16c17-6,1126-378,1142-378C1190,329,1212,336,1212,354z"/>
<path class="st1" d="M2120,2017l-907-282V380l907-308V2017z M2181,32v2023c-1,23-17,33-32,33c-13,0-107-32-123-37
c-126-39-253-78-378-117c-28-9-57-18-84-27c-24-7-50-15-74-23c-107-33-216-66-323-102c-4-1-14-15-14-18V351c2-5,4-11,9-15
c8-9,351-123,486-168c36-13,487-168,501-168C2167,0,2181,13,2181,32z"/>
<polygon points="2411.2,2440.7 1199.5,2054.5 1204.6,373.2 2411.2,757.2 "/>
<g>
<path class="st2" d="M1800.3,1124.6L1681.4,1412l218.6,66.3L1800.3,1124.6z M1729,853.2l156.1,47.3l284.4,1025l-160.3-48.7
l-57.6-210.4L1620.2,1566l-71.3,171.4l-160.4-48.7L1729,853.2z"/>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

View File

@ -1,12 +1,15 @@
import { FunctionalComponent, h } from "preact";
import { TranslationProvider } from "../context/translation";
import AnastasisClient from "../routes/home";
import AnastasisClient from "../pages/home";
const App: FunctionalComponent = () => {
return (
<div id="preact_root">
<AnastasisClient />
</div>
<TranslationProvider>
<div id="app" class="has-navbar-fixed-top">
<AnastasisClient />
</div>
</TranslationProvider>
);
};

View File

@ -0,0 +1,73 @@
/*
This file is part of GNU Taler
(C) 2021 Taler Systems S.A.
GNU Taler is free software; you can redistribute it and/or modify it under the
terms of the GNU General Public License as published by the Free Software
Foundation; either version 3, or (at your option) any later version.
GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
A PARTICULAR PURPOSE. See the GNU General Public License for more details.
You should have received a copy of the GNU General Public License along with
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
/**
*
* @author Sebastian Javier Marchano (sebasjm)
*/
import { h, VNode } from "preact";
import { useState } from "preact/hooks";
import langIcon from '../../assets/icons/languageicon.svg';
import { useTranslationContext } from "../../context/translation";
import { strings as messages } from '../../i18n/strings'
type LangsNames = {
[P in keyof typeof messages]: string
}
const names: LangsNames = {
es: 'Español [es]',
en: 'English [en]',
fr: 'Français [fr]',
de: 'Deutsch [de]',
sv: 'Svenska [sv]',
it: 'Italiano [it]',
}
function getLangName(s: keyof LangsNames | string) {
if (names[s]) return names[s]
return s
}
export function LangSelector(): VNode {
const [updatingLang, setUpdatingLang] = useState(false)
const { lang, changeLanguage } = useTranslationContext()
return <div class="dropdown is-active ">
<div class="dropdown-trigger">
<button class="button has-tooltip-left"
data-tooltip="change language selection"
aria-haspopup="true"
aria-controls="dropdown-menu" onClick={() => setUpdatingLang(!updatingLang)}>
<div class="icon is-small is-left">
<img src={langIcon} />
</div>
<span>{getLangName(lang)}</span>
<div class="icon is-right">
<i class="mdi mdi-chevron-down" />
</div>
</button>
</div>
{updatingLang && <div class="dropdown-menu" id="dropdown-menu" role="menu">
<div class="dropdown-content">
{Object.keys(messages)
.filter((l) => l !== lang)
.map(l => <a key={l} class="dropdown-item" value={l} onClick={() => { changeLanguage(l); setUpdatingLang(false) }}>{getLangName(l)}</a>)}
</div>
</div>}
</div>
}

View File

@ -0,0 +1,58 @@
/*
This file is part of GNU Taler
(C) 2021 Taler Systems S.A.
GNU Taler is free software; you can redistribute it and/or modify it under the
terms of the GNU General Public License as published by the Free Software
Foundation; either version 3, or (at your option) any later version.
GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
A PARTICULAR PURPOSE. See the GNU General Public License for more details.
You should have received a copy of the GNU General Public License along with
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
/**
*
* @author Sebastian Javier Marchano (sebasjm)
*/
import { h, VNode } from 'preact';
import logo from '../../assets/logo.jpeg';
import { LangSelector } from './LangSelector';
interface Props {
onMobileMenu: () => void;
title: string;
}
export function NavigationBar({ onMobileMenu, title }: Props): VNode {
return (<nav class="navbar is-fixed-top" role="navigation" aria-label="main navigation">
<div class="navbar-brand">
<span class="navbar-item" style={{ fontSize: 24, fontWeight: 900 }}>{title}</span>
<a role="button" class="navbar-burger" aria-label="menu" aria-expanded="false" onClick={(e) => {
onMobileMenu()
e.stopPropagation()
}}>
<span aria-hidden="true" />
<span aria-hidden="true" />
<span aria-hidden="true" />
</a>
</div>
<div class="navbar-menu ">
<a class="navbar-start is-justify-content-center is-flex-grow-1" href="https://taler.net">
<img src={logo} style={{ height: 50, maxHeight: 50 }} />
</a>
<div class="navbar-end">
<div class="navbar-item" style={{ paddingTop: 4, paddingBottom: 4 }}>
<LangSelector />
</div>
</div>
</div>
</nav>
);
}

View File

@ -0,0 +1,101 @@
/*
This file is part of GNU Taler
(C) 2021 Taler Systems S.A.
GNU Taler is free software; you can redistribute it and/or modify it under the
terms of the GNU General Public License as published by the Free Software
Foundation; either version 3, or (at your option) any later version.
GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
A PARTICULAR PURPOSE. See the GNU General Public License for more details.
You should have received a copy of the GNU General Public License along with
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
/**
*
* @author Sebastian Javier Marchano (sebasjm)
*/
import { h, VNode } from 'preact';
import { Translate } from '../../i18n';
import { LangSelector } from './LangSelector';
interface Props {
mobile?: boolean;
}
export function Sidebar({ mobile }: Props): VNode {
// const config = useConfigContext();
const config = { version: 'none' }
const process = { env : { __VERSION__: '0.0.0'}}
return (
<aside class="aside is-placed-left is-expanded">
{mobile && <div class="footer" onClick={(e) => { return e.stopImmediatePropagation() }}>
<LangSelector />
</div>}
<div class="aside-tools">
<div class="aside-tools-label">
<div><b>Anastasis</b> Reducer</div>
<div class="is-size-7 has-text-right" style={{ lineHeight: 0, marginTop: -10 }}>
{process.env.__VERSION__} ({config.version})
</div>
</div>
</div>
<div class="menu is-menu-main">
<p class="menu-label">
<Translate>Back up a secret</Translate>
</p>
<ul class="menu-list">
<li>
<div class="has-icon">
<span class="icon"><i class="mdi mdi-square-edit-outline" /></span>
<span class="menu-item-label"><Translate>Location &amp; Currency</Translate></span>
</div>
</li>
<li class="is-active">
<div class="has-icon">
<span class="icon"><i class="mdi mdi-cash-register" /></span>
<span class="menu-item-label"><Translate>Personal information</Translate></span>
</div>
</li>
<li>
<div class="has-icon">
<span class="icon"><i class="mdi mdi-shopping" /></span>
<span class="menu-item-label"><Translate>Authorization methods</Translate></span>
</div>
</li>
<li>
<div class="has-icon">
<span class="icon"><i class="mdi mdi-bank" /></span>
<span class="menu-item-label"><Translate>Recovery policies</Translate></span>
</div>
</li>
<li>
<div class="has-icon">
<span class="icon"><i class="mdi mdi-bank" /></span>
<span class="menu-item-label"><Translate>Enter secrets</Translate></span>
</div>
</li>
<li>
<div class="has-icon">
<span class="icon"><i class="mdi mdi-bank" /></span>
<span class="menu-item-label"><Translate>Payment (optional)</Translate></span>
</div>
</li>
<li>
<div class="has-icon">
<span class="icon"><i class="mdi mdi-cash" /></span>
<span class="menu-item-label">Backup completed</span>
</div>
</li>
</ul>
</div>
</aside>
);
}

View File

@ -0,0 +1,104 @@
/*
This file is part of GNU Taler
(C) 2021 Taler Systems S.A.
GNU Taler is free software; you can redistribute it and/or modify it under the
terms of the GNU General Public License as published by the Free Software
Foundation; either version 3, or (at your option) any later version.
GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
A PARTICULAR PURPOSE. See the GNU General Public License for more details.
You should have received a copy of the GNU General Public License along with
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
import { ComponentChildren, Fragment, h, VNode } from "preact";
import Match from 'preact-router/match';
import { useEffect, useState } from "preact/hooks";
import { NavigationBar } from "./NavigationBar";
import { Sidebar } from "./SideBar";
interface MenuProps {
title: string;
}
function WithTitle({ title, children }: { title: string; children: ComponentChildren }): VNode {
useEffect(() => {
document.title = `Taler Backoffice: ${title}`
}, [title])
return <Fragment>{children}</Fragment>
}
export function Menu({ title }: MenuProps): VNode {
const [mobileOpen, setMobileOpen] = useState(false)
return <Match>{({ path }: { path: string }) => {
const titleWithSubtitle = title // title ? title : (!admin ? getInstanceTitle(path, instance) : getAdminTitle(path, instance))
return (<WithTitle title={titleWithSubtitle}>
<div class={mobileOpen ? "has-aside-mobile-expanded" : ""} onClick={() => setMobileOpen(false)}>
<NavigationBar onMobileMenu={() => setMobileOpen(!mobileOpen)} title={titleWithSubtitle} />
<Sidebar mobile={mobileOpen} />
</div>
</WithTitle>
)
}}</Match>
}
interface NotYetReadyAppMenuProps {
title: string;
onLogout?: () => void;
}
interface NotifProps {
notification?: Notification;
}
export function NotificationCard({ notification: n }: NotifProps): VNode | null {
if (!n) return null
return <div class="notification">
<div class="columns is-vcentered">
<div class="column is-12">
<article class={n.type === 'ERROR' ? "message is-danger" : (n.type === 'WARN' ? "message is-warning" : "message is-info")}>
<div class="message-header">
<p>{n.message}</p>
</div>
{n.description &&
<div class="message-body">
{n.description}
</div>}
</article>
</div>
</div>
</div>
}
export function NotYetReadyAppMenu({ onLogout, title }: NotYetReadyAppMenuProps): VNode {
const [mobileOpen, setMobileOpen] = useState(false)
useEffect(() => {
document.title = `Taler Backoffice: ${title}`
}, [title])
return <div class={mobileOpen ? "has-aside-mobile-expanded" : ""} onClick={() => setMobileOpen(false)}>
<NavigationBar onMobileMenu={() => setMobileOpen(!mobileOpen)} title={title} />
{onLogout && <Sidebar onLogout={onLogout} mobile={mobileOpen} />}
</div>
}
export interface Notification {
message: string;
description?: string | VNode;
type: MessageType;
}
export type ValueOrFunction<T> = T | ((p: T) => T)
export type MessageType = 'INFO' | 'WARN' | 'ERROR' | 'SUCCESS'

View File

@ -0,0 +1,59 @@
/*
This file is part of GNU Taler
(C) 2021 Taler Systems S.A.
GNU Taler is free software; you can redistribute it and/or modify it under the
terms of the GNU General Public License as published by the Free Software
Foundation; either version 3, or (at your option) any later version.
GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
A PARTICULAR PURPOSE. See the GNU General Public License for more details.
You should have received a copy of the GNU General Public License along with
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
/**
*
* @author Sebastian Javier Marchano (sebasjm)
*/
import { createContext, h, VNode } from 'preact'
import { useContext, useEffect } from 'preact/hooks'
import { useLang } from '../hooks'
import * as jedLib from "jed";
import { strings } from "../i18n/strings";
interface Type {
lang: string;
handler: any;
changeLanguage: (l: string) => void;
}
const initial = {
lang: 'en',
handler: null,
changeLanguage: () => {
// do not change anything
}
}
const Context = createContext<Type>(initial)
interface Props {
initial?: string;
children: any;
forceLang?: string;
}
export const TranslationProvider = ({ initial, children, forceLang }: Props): VNode => {
const [lang, changeLanguage] = useLang(initial)
useEffect(() => {
if (forceLang) {
changeLanguage(forceLang)
}
})
const handler = new jedLib.Jed(strings[lang]);
return h(Context.Provider, { value: { lang, handler, changeLanguage }, children });
}
export const useTranslationContext = (): Type => useContext(Context);

View File

@ -0,0 +1,110 @@
/*
This file is part of GNU Taler
(C) 2021 Taler Systems S.A.
GNU Taler is free software; you can redistribute it and/or modify it under the
terms of the GNU General Public License as published by the Free Software
Foundation; either version 3, or (at your option) any later version.
GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
A PARTICULAR PURPOSE. See the GNU General Public License for more details.
You should have received a copy of the GNU General Public License along with
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
/**
*
* @author Sebastian Javier Marchano (sebasjm)
*/
import { StateUpdater, useState } from "preact/hooks";
export type ValueOrFunction<T> = T | ((p: T) => T)
const calculateRootPath = () => {
const rootPath = typeof window !== undefined ? window.location.origin + window.location.pathname : '/'
return rootPath
}
export function useBackendURL(url?: string): [string, boolean, StateUpdater<string>, () => void] {
const [value, setter] = useNotNullLocalStorage('backend-url', url || calculateRootPath())
const [triedToLog, setTriedToLog] = useLocalStorage('tried-login')
const checkedSetter = (v: ValueOrFunction<string>) => {
setTriedToLog('yes')
return setter(p => (v instanceof Function ? v(p) : v).replace(/\/$/, ''))
}
const resetBackend = () => {
setTriedToLog(undefined)
}
return [value, !!triedToLog, checkedSetter, resetBackend]
}
export function useBackendDefaultToken(): [string | undefined, StateUpdater<string | undefined>] {
return useLocalStorage('backend-token')
}
export function useBackendInstanceToken(id: string): [string | undefined, StateUpdater<string | undefined>] {
const [token, setToken] = useLocalStorage(`backend-token-${id}`)
const [defaultToken, defaultSetToken] = useBackendDefaultToken()
// instance named 'default' use the default token
if (id === 'default') {
return [defaultToken, defaultSetToken]
}
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

@ -3,7 +3,7 @@ import { BackupStates, getBackupStartState, getRecoveryStartState, RecoveryState
import { useState } from "preact/hooks";
const reducerBaseUrl = "http://localhost:5000/";
let remoteReducer = true;
const remoteReducer = true;
interface AnastasisState {
reducerState: ReducerState | undefined;
@ -123,7 +123,7 @@ function storageSet(key: string, value: any): void {
function restoreState(): any {
let state: any;
try {
let s = storageGet("anastasisReducerState");
const s = storageGet("anastasisReducerState");
if (s === "undefined") {
state = undefined;
} else if (s) {

View File

@ -0,0 +1,203 @@
/*
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>;
}

View File

@ -0,0 +1,27 @@
# 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/>
#
#, fuzzy
msgid ""
msgstr ""
"Project-Id-Version: Taler Wallet\n"
"Report-Msgid-Bugs-To: taler@gnu.org\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"

View File

@ -0,0 +1,19 @@
/*
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/>
*/
/*eslint quote-props: ["error", "consistent"]*/
export const strings: {[s: string]: any} = {};

View File

@ -0,0 +1,44 @@
/*
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/>
*/
/*eslint quote-props: ["error", "consistent"]*/
export const strings: {[s: string]: any} = {};
strings['de'] = {
"domain": "messages",
"locale_data": {
"messages": {
"": {
"domain": "messages",
"plural_forms": "nplurals=2; plural=(n != 1);",
"lang": ""
},
}
}
};
strings['en'] = {
"domain": "messages",
"locale_data": {
"messages": {
"": {
"domain": "messages",
"plural_forms": "nplurals=2; plural=(n != 1);",
"lang": ""
},
}
}
};

View File

@ -0,0 +1,26 @@
# 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/>
#
#, fuzzy
msgid ""
msgstr ""
"Project-Id-Version: Taler Anastasis\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2016-11-23 00:00+0100\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
"Language: \n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"

View File

@ -1,4 +1,4 @@
import './style/index.css';
import App from './components/app';
import './scss/main.scss';
export default App;

View File

@ -0,0 +1,55 @@
/* eslint-disable @typescript-eslint/camelcase */
import { h, VNode } from "preact";
import { useState } from "preact/hooks";
import { AnastasisReducerApi, ReducerStateRecovery, ReducerStateBackup } from "../../hooks/use-anastasis-reducer";
import { AnastasisClientFrame, withProcessLabel, LabeledInput } from "./index";
export function AttributeEntryScreen(props: AttributeEntryProps): VNode {
const { reducer, reducerState: backupState } = props;
const [attrs, setAttrs] = useState<Record<string, string>>(
props.reducerState.identity_attributes ?? {}
);
return (
<AnastasisClientFrame
title={withProcessLabel(reducer, "Select Country")}
onNext={() => reducer.transition("enter_user_attributes", {
identity_attributes: attrs,
})}
>
{backupState.required_attributes.map((x: any, i: number) => {
return (
<AttributeEntryField
key={i}
isFirst={i == 0}
setValue={(v: string) => setAttrs({ ...attrs, [x.name]: v })}
spec={x}
value={attrs[x.name]} />
);
})}
</AnastasisClientFrame>
);
}
interface AttributeEntryProps {
reducer: AnastasisReducerApi;
reducerState: ReducerStateRecovery | ReducerStateBackup;
}
export interface AttributeEntryFieldProps {
isFirst: boolean;
value: string;
setValue: (newValue: string) => void;
spec: any;
}
export function AttributeEntryField(props: AttributeEntryFieldProps): VNode {
return (
<div>
<LabeledInput
grabFocus={props.isFirst}
label={props.spec.label}
bind={[props.value, props.setValue]}
/>
</div>
);
}

View File

@ -0,0 +1,41 @@
/* eslint-disable @typescript-eslint/camelcase */
import {
encodeCrock,
stringToBytes
} from "@gnu-taler/taler-util";
import { h, VNode } from "preact";
import { useState } from "preact/hooks";
import { AuthMethodSetupProps, AnastasisClientFrame, LabeledInput } from "./index";
export function AuthMethodEmailSetup(props: AuthMethodSetupProps): VNode {
const [email, setEmail] = useState("");
return (
<AnastasisClientFrame hideNav title="Add email authentication">
<p>
For email authentication, you need to provide an email address. When
recovering your secret, you will need to enter the code you receive by
email.
</p>
<div>
<LabeledInput
label="Email address"
grabFocus
bind={[email, setEmail]} />
</div>
<div>
<button onClick={() => props.cancel()}>Cancel</button>
<button
onClick={() => props.addAuthMethod({
authentication_method: {
type: "email",
instructions: `Email to ${email}`,
challenge: encodeCrock(stringToBytes(email)),
},
})}
>
Add
</button>
</div>
</AnastasisClientFrame>
);
}

View File

@ -0,0 +1,68 @@
/* eslint-disable @typescript-eslint/camelcase */
import {
canonicalJson, encodeCrock,
stringToBytes
} from "@gnu-taler/taler-util";
import { h, VNode } from "preact";
import { useState } from "preact/hooks";
import { AuthMethodSetupProps, LabeledInput } from "./index";
export function AuthMethodPostSetup(props: AuthMethodSetupProps): VNode {
const [fullName, setFullName] = useState("");
const [street, setStreet] = useState("");
const [city, setCity] = useState("");
const [postcode, setPostcode] = useState("");
const [country, setCountry] = useState("");
const addPostAuth = () => {
const challengeJson = {
full_name: fullName,
street,
city,
postcode,
country,
};
props.addAuthMethod({
authentication_method: {
type: "email",
instructions: `Letter to address in postal code ${postcode}`,
challenge: encodeCrock(stringToBytes(canonicalJson(challengeJson))),
},
});
};
return (
<div class={style.home}>
<h1>Add {props.method} authentication</h1>
<div>
<p>
For postal letter authentication, you need to provide a postal
address. When recovering your secret, you will be asked to enter a
code that you will receive in a letter to that address.
</p>
<div>
<LabeledInput
grabFocus
label="Full Name"
bind={[fullName, setFullName]} />
</div>
<div>
<LabeledInput label="Street" bind={[street, setStreet]} />
</div>
<div>
<LabeledInput label="City" bind={[city, setCity]} />
</div>
<div>
<LabeledInput label="Postal Code" bind={[postcode, setPostcode]} />
</div>
<div>
<LabeledInput label="Country" bind={[country, setCountry]} />
</div>
<div>
<button onClick={() => props.cancel()}>Cancel</button>
<button onClick={() => addPostAuth()}>Add</button>
</div>
</div>
</div>
);
}

View File

@ -0,0 +1,45 @@
/* eslint-disable @typescript-eslint/camelcase */
import {
encodeCrock,
stringToBytes
} from "@gnu-taler/taler-util";
import { h, VNode } from "preact";
import { useState } from "preact/hooks";
import { AuthMethodSetupProps, AnastasisClientFrame, LabeledInput } from "./index";
export function AuthMethodQuestionSetup(props: AuthMethodSetupProps): VNode {
const [questionText, setQuestionText] = useState("");
const [answerText, setAnswerText] = useState("");
const addQuestionAuth = (): void => props.addAuthMethod({
authentication_method: {
type: "question",
instructions: questionText,
challenge: encodeCrock(stringToBytes(answerText)),
},
});
return (
<AnastasisClientFrame hideNav title="Add Security Question">
<div>
<p>
For security question authentication, you need to provide a question
and its answer. When recovering your secret, you will be shown the
question and you will need to type the answer exactly as you typed it
here.
</p>
<div>
<LabeledInput
label="Security question"
grabFocus
bind={[questionText, setQuestionText]} />
</div>
<div>
<LabeledInput label="Answer" bind={[answerText, setAnswerText]} />
</div>
<div>
<button onClick={() => props.cancel()}>Cancel</button>
<button onClick={() => addQuestionAuth()}>Add</button>
</div>
</div>
</AnastasisClientFrame>
);
}

View File

@ -0,0 +1,50 @@
/* eslint-disable @typescript-eslint/camelcase */
import {
encodeCrock,
stringToBytes
} from "@gnu-taler/taler-util";
import { h, VNode } from "preact";
import { useState, useRef, useLayoutEffect } from "preact/hooks";
import { AuthMethodSetupProps, AnastasisClientFrame } from "./index";
export function AuthMethodSmsSetup(props: AuthMethodSetupProps): VNode {
const [mobileNumber, setMobileNumber] = useState("");
const addSmsAuth = (): void => {
props.addAuthMethod({
authentication_method: {
type: "sms",
instructions: `SMS to ${mobileNumber}`,
challenge: encodeCrock(stringToBytes(mobileNumber)),
},
});
};
const inputRef = useRef<HTMLInputElement>(null);
useLayoutEffect(() => {
inputRef.current?.focus();
}, []);
return (
<AnastasisClientFrame hideNav title="Add SMS authentication">
<div>
<p>
For SMS authentication, you need to provide a mobile number. When
recovering your secret, you will be asked to enter the code you
receive via SMS.
</p>
<label>
Mobile number:{" "}
<input
value={mobileNumber}
ref={inputRef}
style={{ display: "block" }}
autoFocus
onChange={(e) => setMobileNumber((e.target as any).value)}
type="text" />
</label>
<div>
<button onClick={() => props.cancel()}>Cancel</button>
<button onClick={() => addSmsAuth()}>Add</button>
</div>
</div>
</AnastasisClientFrame>
);
}

View File

@ -0,0 +1,116 @@
/* eslint-disable @typescript-eslint/camelcase */
import { h, VNode } from "preact";
import { useState } from "preact/hooks";
import { AuthMethod, ReducerStateBackup } from "anastasis-core";
import { AnastasisReducerApi } from "../../hooks/use-anastasis-reducer";
import { AuthMethodEmailSetup } from "./AuthMethodEmailSetup";
import { AuthMethodPostSetup } from "./AuthMethodPostSetup";
import { AuthMethodQuestionSetup } from "./AuthMethodQuestionSetup";
import { AuthMethodSmsSetup } from "./AuthMethodSmsSetup";
import { AnastasisClientFrame } from "./index";
export function AuthenticationEditorScreen(props: AuthenticationEditorProps): VNode {
const [selectedMethod, setSelectedMethod] = useState<string | undefined>(
undefined
);
const { reducer, backupState } = props;
const providers = backupState.authentication_providers!;
const authAvailableSet = new Set<string>();
for (const provKey of Object.keys(providers)) {
const p = providers[provKey];
if ("http_status" in p && (!("error_code" in p)) && p.methods) {
for (const meth of p.methods) {
authAvailableSet.add(meth.type);
}
}
}
if (selectedMethod) {
const cancel = (): void => setSelectedMethod(undefined);
const addMethod = (args: any): void => {
reducer.transition("add_authentication", args);
setSelectedMethod(undefined);
};
const methodMap: Record<
string, (props: AuthMethodSetupProps) => h.JSX.Element
> = {
sms: AuthMethodSmsSetup,
question: AuthMethodQuestionSetup,
email: AuthMethodEmailSetup,
post: AuthMethodPostSetup,
};
const AuthSetup = methodMap[selectedMethod] ?? AuthMethodNotImplemented;
return (
<AuthSetup
cancel={cancel}
addAuthMethod={addMethod}
method={selectedMethod} />
);
}
function MethodButton(props: { method: string; label: string }): VNode {
return (
<button
disabled={!authAvailableSet.has(props.method)}
onClick={() => {
setSelectedMethod(props.method);
reducer.dismissError();
}}
>
{props.label}
</button>
);
}
const configuredAuthMethods: AuthMethod[] = backupState.authentication_methods ?? [];
const haveMethodsConfigured = configuredAuthMethods.length;
return (
<AnastasisClientFrame title="Backup: Configure Authentication Methods">
<div>
<MethodButton method="sms" label="SMS" />
<MethodButton method="email" label="Email" />
<MethodButton method="question" label="Question" />
<MethodButton method="post" label="Physical Mail" />
<MethodButton method="totp" label="TOTP" />
<MethodButton method="iban" label="IBAN" />
</div>
<h2>Configured authentication methods</h2>
{haveMethodsConfigured ? (
configuredAuthMethods.map((x, i) => {
return (
<p key={i}>
{x.type} ({x.instructions}){" "}
<button
onClick={() => reducer.transition("delete_authentication", {
authentication_method: i,
})}
>
Delete
</button>
</p>
);
})
) : (
<p>No authentication methods configured yet.</p>
)}
</AnastasisClientFrame>
);
}
interface AuthMethodSetupProps {
method: string;
addAuthMethod: (x: any) => void;
cancel: () => void;
}
function AuthMethodNotImplemented(props: AuthMethodSetupProps): VNode {
return (
<AnastasisClientFrame hideNav title={`Add ${props.method} authentication`}>
<p>This auth method is not implemented yet, please choose another one.</p>
<button onClick={() => props.cancel()}>Cancel</button>
</AnastasisClientFrame>
);
}
interface AuthenticationEditorProps {
reducer: AnastasisReducerApi;
backupState: ReducerStateBackup;
}

View File

@ -0,0 +1,23 @@
import { h, VNode } from "preact";
import { BackupReducerProps, AnastasisClientFrame } from "./index";
export function BackupFinishedScreen(props: BackupReducerProps): VNode {
return (<AnastasisClientFrame hideNext title="Backup finished">
<p>
Your backup of secret "{props.backupState.secret_name ?? "??"}" was
successful.
</p>
<p>The backup is stored by the following providers:</p>
<ul>
{Object.keys(props.backupState.success_details!).map((x, i) => {
const sd = props.backupState.success_details![x];
return (
<li key={i}>
{x} (Policy version {sd.policy_version})
</li>
);
})}
</ul>
<button onClick={() => props.reducer.reset()}>Back to start</button>
</AnastasisClientFrame>);
}

View File

@ -0,0 +1,63 @@
import { h, VNode } from "preact";
import { RecoveryReducerProps, AnastasisClientFrame } from "./index";
export function ChallengeOverviewScreen(props: RecoveryReducerProps): VNode {
const { recoveryState, reducer } = props;
const policies = recoveryState.recovery_information!.policies;
const chArr = recoveryState.recovery_information!.challenges;
const challenges: {
[uuid: string]: {
type: string;
instructions: string;
cost: string;
};
} = {};
for (const ch of chArr) {
challenges[ch.uuid] = {
type: ch.type,
cost: ch.cost,
instructions: ch.instructions,
};
}
return (
<AnastasisClientFrame title="Recovery: Solve challenges">
<h2>Policies</h2>
{policies.map((x, i) => {
return (
<div key={i}>
<h3>Policy #{i + 1}</h3>
{x.map((x, j) => {
const ch = challenges[x.uuid];
const feedback = recoveryState.challenge_feedback?.[x.uuid];
return (
<div key={j}
style={{
borderLeft: "2px solid gray",
paddingLeft: "0.5em",
borderRadius: "0.5em",
marginTop: "0.5em",
marginBottom: "0.5em",
}}
>
<h4>
{ch.type} ({ch.instructions})
</h4>
<p>Status: {feedback?.state ?? "unknown"}</p>
{feedback?.state !== "solved" ? (
<button
onClick={() => reducer.transition("select_challenge", {
uuid: x.uuid,
})}
>
Solve
</button>
) : null}
</div>
);
})}
</div>
);
})}
</AnastasisClientFrame>
);
}

View File

@ -0,0 +1,19 @@
import { h, VNode } from "preact";
import { CommonReducerProps, AnastasisClientFrame, withProcessLabel } from "./index";
export function ContinentSelectionScreen(props: CommonReducerProps): VNode {
const { reducer, reducerState } = props;
const sel = (x: string): void => reducer.transition("select_continent", { continent: x });
return (
<AnastasisClientFrame
hideNext
title={withProcessLabel(reducer, "Select Continent")}
>
{reducerState.continents.map((x: any) => (
<button onClick={() => sel(x.name)} key={x.name}>
{x.name}
</button>
))}
</AnastasisClientFrame>
);
}

View File

@ -0,0 +1,23 @@
/* eslint-disable @typescript-eslint/camelcase */
import { h, VNode } from "preact";
import { CommonReducerProps, AnastasisClientFrame, withProcessLabel } from "./index";
export function CountrySelectionScreen(props: CommonReducerProps): VNode {
const { reducer, reducerState } = props;
const sel = (x: any): void => reducer.transition("select_country", {
country_code: x.code,
currencies: [x.currency],
});
return (
<AnastasisClientFrame
hideNext
title={withProcessLabel(reducer, "Select Country")}
>
{reducerState.countries.map((x: any) => (
<button onClick={() => sel(x)} key={x.name}>
{x.name} ({x.currency})
</button>
))}
</AnastasisClientFrame>
);
}

View File

@ -0,0 +1,27 @@
import { h, VNode } from "preact";
import { BackupReducerProps, AnastasisClientFrame } from "./index";
export function PoliciesPayingScreen(props: BackupReducerProps): VNode {
const payments = props.backupState.policy_payment_requests ?? [];
return (
<AnastasisClientFrame hideNext title="Backup: Recovery Document Payments">
<p>
Some of the providers require a payment to store the encrypted
recovery document.
</p>
<ul>
{payments.map((x, i) => {
return (
<li key={i}>
{x.provider}: {x.payto}
</li>
);
})}
</ul>
<button onClick={() => props.reducer.transition("pay", {})}>
Check payment status now
</button>
</AnastasisClientFrame>
);
}

View File

@ -0,0 +1,17 @@
import {
bytesToString,
decodeCrock
} from "@gnu-taler/taler-util";
import { h, VNode } from "preact";
import { RecoveryReducerProps, AnastasisClientFrame } from "./index";
export function RecoveryFinishedScreen(props: RecoveryReducerProps): VNode {
return (
<AnastasisClientFrame title="Recovery Finished" hideNext>
<h1>Recovery Finished</h1>
<p>
Secret: {bytesToString(decodeCrock(props.recoveryState.core_secret?.value!))}
</p>
</AnastasisClientFrame>
);
}

View File

@ -0,0 +1,43 @@
/* eslint-disable @typescript-eslint/camelcase */
import { h, VNode } from "preact";
import { BackupReducerProps, AnastasisClientFrame } from "./index";
export function ReviewPoliciesScreen(props: BackupReducerProps): VNode {
const { reducer, backupState } = props;
const authMethods = backupState.authentication_methods!;
return (
<AnastasisClientFrame title="Backup: Review Recovery Policies">
{backupState.policies?.map((p, i) => {
const policyName = p.methods
.map((x, i) => authMethods[x.authentication_method].type)
.join(" + ");
return (
<div key={i}>
{/* <div key={i} class={style.policy}> */}
<h3>
Policy #{i + 1}: {policyName}
</h3>
Required Authentications:
<ul>
{p.methods.map((x, i) => {
const m = authMethods[x.authentication_method];
return (
<li key={i}>
{m.type} ({m.instructions}) at provider {x.provider}
</li>
);
})}
</ul>
<div>
<button
onClick={() => reducer.transition("delete_policy", { policy_index: i })}
>
Delete Policy
</button>
</div>
</div>
);
})}
</AnastasisClientFrame>
);
}

View File

@ -0,0 +1,53 @@
/* eslint-disable @typescript-eslint/camelcase */
import {
encodeCrock,
stringToBytes
} from "@gnu-taler/taler-util";
import { h, VNode } from "preact";
import { useState } from "preact/hooks";
import { BackupReducerProps, AnastasisClientFrame, LabeledInput } from "./index";
export function SecretEditorScreen(props: BackupReducerProps): VNode {
const { reducer } = props;
const [secretName, setSecretName] = useState(
props.backupState.secret_name ?? ""
);
const [secretValue, setSecretValue] = useState(
props.backupState.core_secret?.value ?? "" ?? ""
);
const secretNext = (): void => {
reducer.runTransaction(async (tx) => {
await tx.transition("enter_secret_name", {
name: secretName,
});
await tx.transition("enter_secret", {
secret: {
value: encodeCrock(stringToBytes(secretValue)),
mime: "text/plain",
},
expiration: {
t_ms: new Date().getTime() + 1000 * 60 * 60 * 24 * 365 * 5,
},
});
await tx.transition("next", {});
});
};
return (
<AnastasisClientFrame
title="Backup: Provide secret"
onNext={() => secretNext()}
>
<div>
<LabeledInput
label="Secret Name:"
grabFocus
bind={[secretName, setSecretName]} />
</div>
<div>
<LabeledInput
label="Secret Value:"
bind={[secretValue, setSecretValue]} />
</div>
</AnastasisClientFrame>
);
}

View File

@ -0,0 +1,66 @@
/* eslint-disable @typescript-eslint/camelcase */
import { h, VNode } from "preact";
import { useState } from "preact/hooks";
import { RecoveryReducerProps, AnastasisClientFrame } from "./index";
export function SecretSelectionScreen(props: RecoveryReducerProps): VNode {
const { reducer, recoveryState } = props;
const [selectingVersion, setSelectingVersion] = useState<boolean>(false);
const [otherVersion, setOtherVersion] = useState<number>(
recoveryState.recovery_document?.version ?? 0
);
const recoveryDocument = recoveryState.recovery_document!;
const [otherProvider, setOtherProvider] = useState<string>("");
function selectVersion(p: string, n: number): void {
reducer.runTransaction(async (tx) => {
await tx.transition("change_version", {
version: n,
provider_url: p,
});
setSelectingVersion(false);
});
}
if (selectingVersion) {
return (
<AnastasisClientFrame hideNav title="Recovery: Select secret">
<p>Select a different version of the secret</p>
<select onChange={(e) => setOtherProvider((e.target as any).value)}>
{Object.keys(recoveryState.authentication_providers ?? {}).map(
(x, i) => (
<option key={i} selected={x === recoveryDocument.provider_url} value={x}>
{x}
</option>
)
)}
</select>
<div>
<input
value={otherVersion}
onChange={(e) => setOtherVersion(Number((e.target as HTMLInputElement).value))}
type="number" />
<button onClick={() => selectVersion(otherProvider, otherVersion)}>
Use this version
</button>
</div>
<div>
<button onClick={() => selectVersion(otherProvider, 0)}>
Use latest version
</button>
</div>
<div>
<button onClick={() => setSelectingVersion(false)}>Cancel</button>
</div>
</AnastasisClientFrame>
);
}
return (
<AnastasisClientFrame title="Recovery: Select secret">
<p>Provider: {recoveryDocument.provider_url}</p>
<p>Secret version: {recoveryDocument.version}</p>
<p>Secret name: {recoveryDocument.version}</p>
<button onClick={() => setSelectingVersion(true)}>
Select different secret
</button>
</AnastasisClientFrame>
);
}

View File

@ -0,0 +1,22 @@
import { h, VNode } from "preact";
import { useState } from "preact/hooks";
import { AnastasisClientFrame, LabeledInput } from "./index";
import { SolveEntryProps } from "./SolveScreen";
export function SolveEmailEntry(props: SolveEntryProps): VNode {
const [answer, setAnswer] = useState("");
const { reducer, challenge, feedback } = props;
const next = (): void => reducer.transition("solve_challenge", {
answer,
});
return (
<AnastasisClientFrame
title="Recovery: Solve challenge"
onNext={() => next()}
>
<p>Feedback: {JSON.stringify(feedback)}</p>
<p>{challenge.instructions}</p>
<LabeledInput label="Answer" grabFocus bind={[answer, setAnswer]} />
</AnastasisClientFrame>
);
}

View File

@ -0,0 +1,22 @@
import { h, VNode } from "preact";
import { useState } from "preact/hooks";
import { AnastasisClientFrame, LabeledInput } from "./index";
import { SolveEntryProps } from "./SolveScreen";
export function SolvePostEntry(props: SolveEntryProps): VNode {
const [answer, setAnswer] = useState("");
const { reducer, challenge, feedback } = props;
const next = (): void => reducer.transition("solve_challenge", {
answer,
});
return (
<AnastasisClientFrame
title="Recovery: Solve challenge"
onNext={() => next()}
>
<p>Feedback: {JSON.stringify(feedback)}</p>
<p>{challenge.instructions}</p>
<LabeledInput label="Answer" grabFocus bind={[answer, setAnswer]} />
</AnastasisClientFrame>
);
}

View File

@ -0,0 +1,22 @@
import { h, VNode } from "preact";
import { useState } from "preact/hooks";
import { AnastasisClientFrame, LabeledInput } from "./index";
import { SolveEntryProps } from "./SolveScreen";
export function SolveQuestionEntry(props: SolveEntryProps): VNode {
const [answer, setAnswer] = useState("");
const { reducer, challenge, feedback } = props;
const next = (): void => reducer.transition("solve_challenge", {
answer,
});
return (
<AnastasisClientFrame
title="Recovery: Solve challenge"
onNext={() => next()}
>
<p>Feedback: {JSON.stringify(feedback)}</p>
<p>Question: {challenge.instructions}</p>
<LabeledInput label="Answer" grabFocus bind={[answer, setAnswer]} />
</AnastasisClientFrame>
);
}

View File

@ -0,0 +1,41 @@
import { h, VNode } from "preact";
import { AnastasisReducerApi, ChallengeFeedback, ChallengeInfo } from "../../hooks/use-anastasis-reducer";
import { SolveEmailEntry } from "./SolveEmailEntry";
import { SolvePostEntry } from "./SolvePostEntry";
import { SolveQuestionEntry } from "./SolveQuestionEntry";
import { SolveSmsEntry } from "./SolveSmsEntry";
import { SolveUnsupportedEntry } from "./SolveUnsupportedEntry";
import { RecoveryReducerProps } from "./index";
export function SolveScreen(props: RecoveryReducerProps): VNode {
const chArr = props.recoveryState.recovery_information!.challenges;
const challengeFeedback = props.recoveryState.challenge_feedback ?? {};
const selectedUuid = props.recoveryState.selected_challenge_uuid!;
const challenges: {
[uuid: string]: ChallengeInfo;
} = {};
for (const ch of chArr) {
challenges[ch.uuid] = ch;
}
const selectedChallenge = challenges[selectedUuid];
const dialogMap: Record<string, (p: SolveEntryProps) => h.JSX.Element> = {
question: SolveQuestionEntry,
sms: SolveSmsEntry,
email: SolveEmailEntry,
post: SolvePostEntry,
};
const SolveDialog = dialogMap[selectedChallenge.type] ?? SolveUnsupportedEntry;
return (
<SolveDialog
challenge={selectedChallenge}
reducer={props.reducer}
feedback={challengeFeedback[selectedUuid]} />
);
}
export interface SolveEntryProps {
reducer: AnastasisReducerApi;
challenge: ChallengeInfo;
feedback?: ChallengeFeedback;
}

View File

@ -0,0 +1,22 @@
import { h, VNode } from "preact";
import { useState } from "preact/hooks";
import { AnastasisClientFrame, LabeledInput } from "./index";
import { SolveEntryProps } from "./SolveScreen";
export function SolveSmsEntry(props: SolveEntryProps): VNode {
const [answer, setAnswer] = useState("");
const { reducer, challenge, feedback } = props;
const next = (): void => reducer.transition("solve_challenge", {
answer,
});
return (
<AnastasisClientFrame
title="Recovery: Solve challenge"
onNext={() => next()}
>
<p>Feedback: {JSON.stringify(feedback)}</p>
<p>{challenge.instructions}</p>
<LabeledInput label="Answer" grabFocus bind={[answer, setAnswer]} />
</AnastasisClientFrame>
);
}

View File

@ -0,0 +1,12 @@
import { h, VNode } from "preact";
import { AnastasisClientFrame } from "./index";
import { SolveEntryProps } from "./SolveScreen";
export function SolveUnsupportedEntry(props: SolveEntryProps): VNode {
return (
<AnastasisClientFrame hideNext title="Recovery: Solve challenge">
<p>{JSON.stringify(props.challenge)}</p>
<p>Challenge not supported.</p>
</AnastasisClientFrame>
);
}

View File

@ -0,0 +1,14 @@
import { h, VNode } from "preact";
import { AnastasisReducerApi } from "../../hooks/use-anastasis-reducer";
import { AnastasisClientFrame } from "./index";
export function StartScreen(props: { reducer: AnastasisReducerApi; }): VNode {
return (
<AnastasisClientFrame hideNav title="Home">
<button autoFocus onClick={() => props.reducer.startBackup()}>
Backup
</button>
<button onClick={() => props.reducer.startRecover()}>Recover</button>
</AnastasisClientFrame>
);
}

View File

@ -0,0 +1,25 @@
import { h, VNode } from "preact";
import { BackupReducerProps, AnastasisClientFrame } from "./index";
export function TruthsPayingScreen(props: BackupReducerProps): VNode {
const payments = props.backupState.payments ?? [];
return (
<AnastasisClientFrame
hideNext
title="Backup: Authentication Storage Payments"
>
<p>
Some of the providers require a payment to store the encrypted
authentication information.
</p>
<ul>
{payments.map((x, i) => {
return <li key={i}>{x}</li>;
})}
</ul>
<button onClick={() => props.reducer.transition("pay", {})}>
Check payment status now
</button>
</AnastasisClientFrame>
);
}

View File

@ -0,0 +1,248 @@
import {
ComponentChildren, createContext,
Fragment, FunctionalComponent, h, VNode
} from "preact";
import { useContext, useLayoutEffect, useRef } from "preact/hooks";
import { Menu } from "../../components/menu";
import {
BackupStates, RecoveryStates,
ReducerStateBackup,
ReducerStateRecovery,
} from "anastasis-core";
import {
AnastasisReducerApi,
useAnastasisReducer
} from "../../hooks/use-anastasis-reducer";
import { AttributeEntryScreen } from "./AttributeEntryScreen";
import { AuthenticationEditorScreen } from "./AuthenticationEditorScreen";
import { BackupFinishedScreen } from "./BackupFinishedScreen";
import { ChallengeOverviewScreen } from "./ChallengeOverviewScreen";
import { ContinentSelectionScreen } from "./ContinentSelectionScreen";
import { CountrySelectionScreen } from "./CountrySelectionScreen";
import { PoliciesPayingScreen } from "./PoliciesPayingScreen";
import { RecoveryFinishedScreen } from "./RecoveryFinishedScreen";
import { ReviewPoliciesScreen } from "./ReviewPoliciesScreen";
import { SecretEditorScreen } from "./SecretEditorScreen";
import { SecretSelectionScreen } from "./SecretSelectionScreen";
import { SolveScreen } from "./SolveScreen";
import { StartScreen } from "./StartScreen";
import { TruthsPayingScreen } from "./TruthsPayingScreen";
const WithReducer = createContext<AnastasisReducerApi | undefined>(undefined);
function isBackup(reducer: AnastasisReducerApi): boolean {
return !!reducer.currentReducerState?.backup_state;
}
export interface CommonReducerProps {
reducer: AnastasisReducerApi;
reducerState: ReducerStateBackup | ReducerStateRecovery;
}
export function withProcessLabel(reducer: AnastasisReducerApi, text: string): string {
if (isBackup(reducer)) {
return `Backup: ${text}`;
}
return `Recovery: ${text}`;
}
export interface BackupReducerProps {
reducer: AnastasisReducerApi;
backupState: ReducerStateBackup;
}
export interface RecoveryReducerProps {
reducer: AnastasisReducerApi;
recoveryState: ReducerStateRecovery;
}
interface AnastasisClientFrameProps {
onNext?(): void;
title: string;
children: ComponentChildren;
/**
* Should back/next buttons be provided?
*/
hideNav?: boolean;
/**
* Hide only the "next" button.
*/
hideNext?: boolean;
}
export function AnastasisClientFrame(props: AnastasisClientFrameProps): VNode {
const reducer = useContext(WithReducer);
if (!reducer) {
return <p>Fatal: Reducer must be in context.</p>;
}
const next = (): void => {
if (props.onNext) {
props.onNext();
} else {
reducer.transition("next", {});
}
};
const handleKeyPress = (e: h.JSX.TargetedKeyboardEvent<HTMLDivElement>): void => {
console.log("Got key press", e.key);
// FIXME: By default, "next" action should be executed here
};
return (<Fragment>
<Menu title="Anastasis" />
<section class="section">
<div onKeyPress={(e) => handleKeyPress(e)}> {/* class={style.home} */}
<button onClick={() => reducer.reset()}>Reset session</button>
<h1>{props.title}</h1>
<ErrorBanner reducer={reducer} />
{props.children}
{!props.hideNav ? (
<div>
<button onClick={() => reducer.back()}>Back</button>
{!props.hideNext ? (
<button onClick={next}>Next</button>
) : null}
</div>
) : null}
</div>
</section>
</Fragment>
);
}
const AnastasisClient: FunctionalComponent = () => {
const reducer = useAnastasisReducer();
return (
<WithReducer.Provider value={reducer}>
<AnastasisClientImpl />
</WithReducer.Provider>
);
};
const AnastasisClientImpl: FunctionalComponent = () => {
const reducer = useContext(WithReducer)!;
const reducerState = reducer.currentReducerState;
if (!reducerState) {
return <StartScreen reducer={reducer} />;
}
console.log("state", reducer.currentReducerState);
if (
reducerState.backup_state === BackupStates.ContinentSelecting ||
reducerState.recovery_state === RecoveryStates.ContinentSelecting
) {
return <ContinentSelectionScreen reducer={reducer} reducerState={reducerState} />;
}
if (
reducerState.backup_state === BackupStates.CountrySelecting ||
reducerState.recovery_state === RecoveryStates.CountrySelecting
) {
return <CountrySelectionScreen reducer={reducer} reducerState={reducerState} />;
}
if (
reducerState.backup_state === BackupStates.UserAttributesCollecting ||
reducerState.recovery_state === RecoveryStates.UserAttributesCollecting
) {
return <AttributeEntryScreen reducer={reducer} reducerState={reducerState} />;
}
if (reducerState.backup_state === BackupStates.AuthenticationsEditing) {
return (
<AuthenticationEditorScreen backupState={reducerState} reducer={reducer} />
);
}
if (reducerState.backup_state === BackupStates.PoliciesReviewing) {
return <ReviewPoliciesScreen reducer={reducer} backupState={reducerState} />;
}
if (reducerState.backup_state === BackupStates.SecretEditing) {
return <SecretEditorScreen reducer={reducer} backupState={reducerState} />;
}
if (reducerState.backup_state === BackupStates.BackupFinished) {
const backupState: ReducerStateBackup = reducerState;
return <BackupFinishedScreen reducer={reducer} backupState={backupState} />;
}
if (reducerState.backup_state === BackupStates.TruthsPaying) {
return <TruthsPayingScreen reducer={reducer} backupState={reducerState} />
}
if (reducerState.backup_state === BackupStates.PoliciesPaying) {
const backupState: ReducerStateBackup = reducerState;
return <PoliciesPayingScreen reducer={reducer} backupState={backupState} />
}
if (reducerState.recovery_state === RecoveryStates.SecretSelecting) {
return <SecretSelectionScreen reducer={reducer} recoveryState={reducerState} />;
}
if (reducerState.recovery_state === RecoveryStates.ChallengeSelecting) {
return <ChallengeOverviewScreen reducer={reducer} recoveryState={reducerState} />;
}
if (reducerState.recovery_state === RecoveryStates.ChallengeSolving) {
return <SolveScreen reducer={reducer} recoveryState={reducerState} />
}
if (reducerState.recovery_state === RecoveryStates.RecoveryFinished) {
return <RecoveryFinishedScreen reducer={reducer} recoveryState={reducerState} />
}
console.log("unknown state", reducer.currentReducerState);
return (
<AnastasisClientFrame hideNav title="Bug">
<p>Bug: Unknown state.</p>
<button onClick={() => reducer.reset()}>Reset</button>
</AnastasisClientFrame>
);
};
interface LabeledInputProps {
label: string;
grabFocus?: boolean;
bind: [string, (x: string) => void];
}
export function LabeledInput(props: LabeledInputProps): VNode {
const inputRef = useRef<HTMLInputElement>(null);
useLayoutEffect(() => {
if (props.grabFocus) {
inputRef.current?.focus();
}
}, [props.grabFocus]);
return (
<label>
{props.label}
<input
value={props.bind[0]}
onChange={(e) => props.bind[1]((e.target as HTMLInputElement).value)}
ref={inputRef}
style={{ display: "block" }}
/>
</label>
);
}
interface ErrorBannerProps {
reducer: AnastasisReducerApi;
}
/**
* Show a dismissable error banner if there is a current error.
*/
function ErrorBanner(props: ErrorBannerProps): VNode | null {
const currentError = props.reducer.currentError;
if (currentError) {
return (
<div id="error"> {/* style.error */}
<p>Error: {JSON.stringify(currentError)}</p>
<button onClick={() => props.reducer.dismissError()}>
Dismiss Error
</button>
</div>
);
}
return null;
}
export default AnastasisClient;

View File

@ -1,10 +1,9 @@
import { FunctionalComponent, h } from 'preact';
import { Link } from 'preact-router/match';
import style from './style.css';
const Notfound: FunctionalComponent = () => {
return (
<div class={style.notfound}>
<div>
<h1>Error 404</h1>
<p>That page doesn&apos;t exist.</p>
<Link href="/">

View File

@ -1,6 +1,5 @@
import { FunctionalComponent, h } from 'preact';
import { useEffect, useState } from 'preact/hooks';
import style from './style.css';
interface Props {
user: string;
@ -27,7 +26,7 @@ const Profile: FunctionalComponent<Props> = (props: Props) => {
};
return (
<div class={style.profile}>
<div>
<h1>Profile: {user}</h1>
<p>This is the user profile for a user named {user}.</p>

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,71 @@
.rdp-picker {
display: flex;
height: 175px;
}
@media (max-width: 400px) {
.rdp-picker {
width: 250px;
}
}
.rdp-masked-div {
overflow: hidden;
height: 175px;
position: relative;
}
.rdp-column-container {
flex-grow: 1;
display: inline-block;
}
.rdp-column {
position: absolute;
z-index: 0;
width: 100%;
}
.rdp-reticule {
border: 0;
border-top: 2px solid rgba(109, 202, 236, 1);
height: 2px;
position: absolute;
width: 80%;
margin: 0;
z-index: 100;
left: 50%;
-webkit-transform: translateX(-50%);
transform: translateX(-50%);
}
.rdp-text-overlay {
position: absolute;
display: flex;
align-items: center;
justify-content: center;
height: 35px;
font-size: 20px;
left: 50%;
-webkit-transform: translateX(-50%);
transform: translateX(-50%);
}
.rdp-cell div {
font-size: 17px;
color: gray;
font-style: italic;
}
.rdp-cell {
display: flex;
align-items: center;
justify-content: center;
height: 35px;
font-size: 18px;
}
.rdp-center {
font-size: 25px;
}

View File

@ -0,0 +1,186 @@
/*
This file is part of GNU Taler
(C) 2021 Taler Systems S.A.
GNU Taler is free software; you can redistribute it and/or modify it under the
terms of the GNU General Public License as published by the Free Software
Foundation; either version 3, or (at your option) any later version.
GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
A PARTICULAR PURPOSE. See the GNU General Public License for more details.
You should have received a copy of the GNU General Public License along with
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
/**
*
* @author Sebastian Javier Marchano (sebasjm)
*/
@include desktop {
html {
&.has-aside-left {
&.has-aside-expanded {
nav.navbar,
body {
padding-left: $aside-width;
}
}
aside.is-placed-left {
display: block;
}
}
}
aside.aside.is-expanded {
width: $aside-width;
.menu-list {
@include icon-with-update-mark($aside-icon-width);
span.menu-item-label {
display: inline-block;
}
li.is-active {
ul {
display: block;
}
background-color: $body-background-color;
}
}
}
}
aside.aside {
display: none;
position: fixed;
top: 0;
left: 0;
z-index: 40;
height: 100vh;
padding: 0;
box-shadow: $aside-box-shadow;
background: $aside-background-color;
.aside-tools {
display: flex;
flex-direction: row;
width: 100%;
background-color: $aside-tools-background-color;
color: $aside-tools-color;
line-height: $navbar-height;
height: $navbar-height;
padding-left: $default-padding * 0.5;
flex: 1;
.icon {
margin-right: $default-padding * 0.5;
}
}
.menu-list {
li {
a {
&.has-dropdown-icon {
position: relative;
padding-right: $aside-icon-width;
.dropdown-icon {
position: absolute;
top: $size-base * 0.5;
right: 0;
}
}
}
ul {
display: none;
border-left: 0;
background-color: darken($base-color, 2.5%);
padding-left: 0;
margin: 0 0 $default-padding * 0.5;
li {
a {
padding: $default-padding * 0.5 0 $default-padding * 0.5
$default-padding * 0.5;
font-size: $aside-submenu-font-size;
&.has-icon {
padding-left: 0;
}
&.is-active {
&:not(:hover) {
background: transparent;
}
}
}
}
}
}
}
.menu-label {
padding: 0 $default-padding * 0.5;
margin-top: $default-padding * 0.5;
margin-bottom: $default-padding * 0.5;
}
}
@include touch {
nav.navbar {
@include transition(margin-left);
}
aside.aside {
@include transition(left);
}
html.has-aside-mobile-transition {
body {
overflow-x: hidden;
}
body,
nav.navbar {
width: 100vw;
}
aside.aside {
width: $aside-mobile-width;
display: block;
left: $aside-mobile-width * -1;
.image {
img {
max-width: $aside-mobile-width * 0.33;
}
}
.menu-list {
li.is-active {
ul {
display: block;
}
background-color: $body-background-color;
}
li {
@include icon-with-update-mark($aside-icon-width);
margin-top: 8px;
margin-bottom: 8px;
}
a {
span.menu-item-label {
display: inline-block;
}
}
}
}
}
div.has-aside-mobile-expanded {
nav.navbar {
margin-left: $aside-mobile-width;
}
aside.aside {
left: 0;
}
}
}

View File

@ -0,0 +1,69 @@
/*
This file is part of GNU Taler
(C) 2021 Taler Systems S.A.
GNU Taler is free software; you can redistribute it and/or modify it under the
terms of the GNU General Public License as published by the Free Software
Foundation; either version 3, or (at your option) any later version.
GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
A PARTICULAR PURPOSE. See the GNU General Public License for more details.
You should have received a copy of the GNU General Public License along with
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
/**
*
* @author Sebastian Javier Marchano (sebasjm)
*/
.card:not(:last-child) {
margin-bottom: $default-padding;
}
.card {
border-radius: $radius-large;
border: $card-border;
&.has-table {
.card-content {
padding: 0;
}
.b-table {
border-radius: $radius-large;
overflow: hidden;
}
}
&.is-card-widget {
.card-content {
padding: $default-padding * .5;
}
}
.card-header {
border-bottom: 1px solid $base-color-light;
}
.card-content {
hr {
margin-left: $card-content-padding * -1;
margin-right: $card-content-padding * -1;
}
}
.is-widget-icon {
.icon {
width: 5rem;
height: 5rem;
}
}
.is-widget-label {
.subtitle {
color: $grey;
}
}
}

View File

@ -0,0 +1,254 @@
/*
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/>
*/
:root {
--primary-color: #3298dc;
--primary-text-color-dark: rgba(0,0,0,.87);
--secondary-text-color-dark: rgba(0,0,0,.57);
--disabled-text-color-dark: rgba(0,0,0,.13);
--primary-text-color-light: rgba(255,255,255,.87);
--secondary-text-color-light: rgba(255,255,255,.57);
--disabled-text-color-light: rgba(255,255,255,.13);
--font-stack: 'Roboto', 'Helvetica Neue', Helvetica, Arial, sans-serif;
--primary-card-color: #fff;
--primary-background-color: #f2f2f2;
--box-shadow-lvl-1: 0 1px 3px rgba(0, 0, 0, 0.12),
0 1px 2px rgba(0, 0, 0, 0.24);
--box-shadow-lvl-2: 0 3px 6px rgba(0, 0, 0, 0.16),
0 3px 6px rgba(0, 0, 0, 0.23);
--box-shadow-lvl-3: 0 10px 20px rgba(0, 0, 0, 0.19),
0 6px 6px rgba(0, 0, 0, 0.23);
--box-shadow-lvl-4: 0 14px 28px rgba(0, 0, 0, 0.25),
0 10px 10px rgba(0, 0, 0, 0.22);
}
.datePicker {
text-align: left;
background: var(--primary-card-color);
border-radius: 3px;
z-index: 200;
position: fixed;
height: auto;
max-height: 90vh;
width: 90vw;
max-width: 448px;
transform-origin: top left;
transition: transform .22s ease-in-out, opacity .22s ease-in-out;
top: 50%;
left: 50%;
opacity: 0;
transform: scale(0) translate(-50%, -50%);
user-select: none;
&.datePicker--opened {
opacity: 1;
transform: scale(1) translate(-50%, -50%);
}
.datePicker--titles {
border-top-left-radius: 3px;
border-top-right-radius: 3px;
padding: 24px;
height: 100px;
background: var(--primary-color);
h2, h3 {
cursor: pointer;
color: #fff;
line-height: 1;
padding: 0;
margin: 0;
font-size: 32px;
}
h3 {
color: rgba(255,255,255,.57);
font-size: 18px;
padding-bottom: 2px;
}
}
nav {
padding: 20px;
height: 56px;
h4 {
width: calc(100% - 60px);
text-align: center;
display: inline-block;
padding: 0;
font-size: 14px;
line-height: 24px;
margin: 0;
position: relative;
top: -9px;
color: var(--primary-text-color);
}
i {
cursor: pointer;
color: var(--secondary-text-color);
font-size: 26px;
user-select: none;
border-radius: 50%;
&:hover {
background: var(--disabled-text-color-dark);
}
}
}
.datePicker--scroll {
overflow-y: auto;
max-height: calc(90vh - 56px - 100px);
}
.datePicker--calendar {
padding: 0 20px;
.datePicker--dayNames {
width: 100%;
display: grid;
text-align: center;
// there's probably a better way to do this, but wanted to try out CSS grid
grid-template-columns: calc(100% / 7) calc(100% / 7) calc(100% / 7) calc(100% / 7) calc(100% / 7) calc(100% / 7) calc(100% / 7);
span {
color: var(--secondary-text-color-dark);
font-size: 14px;
line-height: 42px;
display: inline-grid;
}
}
.datePicker--days {
width: 100%;
display: grid;
text-align: center;
grid-template-columns: calc(100% / 7) calc(100% / 7) calc(100% / 7) calc(100% / 7) calc(100% / 7) calc(100% / 7) calc(100% / 7);
span {
color: var(--primary-text-color-dark);
line-height: 42px;
font-size: 14px;
display: inline-grid;
transition: color .22s;
height: 42px;
position: relative;
cursor: pointer;
user-select: none;
border-radius: 50%;
&::before {
content: '';
position: absolute;
z-index: -1;
height: 42px;
width: 42px;
left: calc(50% - 21px);
background: var(--primary-color);
border-radius: 50%;
transition: transform .22s, opacity .22s;
transform: scale(0);
opacity: 0;
}
&[disabled=true] {
cursor: unset;
}
&.datePicker--today {
font-weight: 700;
}
&.datePicker--selected {
color: rgba(255,255,255,.87);
&:before {
transform: scale(1);
opacity: 1;
}
}
}
}
}
.datePicker--selectYear {
padding: 0 20px;
display: block;
width: 100%;
text-align: center;
max-height: 362px;
span {
display: block;
width: 100%;
font-size: 24px;
margin: 20px auto;
cursor: pointer;
&.selected {
font-size: 42px;
color: var(--primary-color);
}
}
}
div.datePicker--actions {
width: 100%;
padding: 8px;
text-align: right;
button {
margin-bottom: 0;
font-size: 15px;
cursor: pointer;
color: var(--primary-text-color);
border: none;
margin-left: 8px;
min-width: 64px;
line-height: 36px;
background-color: transparent;
appearance: none;
padding: 0 16px;
border-radius: 3px;
transition: background-color .13s;
&:hover, &:focus {
outline: none;
background-color: var(--disabled-text-color-dark);
}
}
}
}
.datePicker--background {
z-index: 199;
position: fixed;
top: 0;
left: 0;
bottom: 0;
right: 0;
background: rgba(0,0,0,.52);
animation: fadeIn .22s forwards;
}

View File

@ -0,0 +1,35 @@
/*
This file is part of GNU Taler
(C) 2021 Taler Systems S.A.
GNU Taler is free software; you can redistribute it and/or modify it under the
terms of the GNU General Public License as published by the Free Software
Foundation; either version 3, or (at your option) any later version.
GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
A PARTICULAR PURPOSE. See the GNU General Public License for more details.
You should have received a copy of the GNU General Public License along with
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
/**
*
* @author Sebastian Javier Marchano (sebasjm)
*/
footer.footer {
.logo {
img {
width: auto;
height: $footer-logo-height;
}
}
}
@include mobile {
.footer-copyright {
text-align: center;
}
}

View File

@ -0,0 +1,64 @@
/*
This file is part of GNU Taler
(C) 2021 Taler Systems S.A.
GNU Taler is free software; you can redistribute it and/or modify it under the
terms of the GNU General Public License as published by the Free Software
Foundation; either version 3, or (at your option) any later version.
GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
A PARTICULAR PURPOSE. See the GNU General Public License for more details.
You should have received a copy of the GNU General Public License along with
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
/**
*
* @author Sebastian Javier Marchano (sebasjm)
*/
.field {
&.has-check {
.field-body {
margin-top: $default-padding * .125;
}
}
.control {
.mdi-24px.mdi-set, .mdi-24px.mdi:before {
font-size: inherit;
}
}
}
.upload {
.upload-draggable {
display: block;
}
}
.input, .textarea, select {
box-shadow: none;
&:focus, &:active {
box-shadow: none!important;
}
}
.switch input[type=checkbox]+.check:before {
box-shadow: none;
}
.switch, .b-checkbox.checkbox {
input[type=checkbox] {
&:focus + .check, &:focus:checked + .check {
box-shadow: none!important;
}
}
}
.b-checkbox.checkbox input[type=checkbox], .b-radio.radio input[type=radio] {
&+.check {
border: $checkbox-border;
}
}

View File

@ -0,0 +1,55 @@
/*
This file is part of GNU Taler
(C) 2021 Taler Systems S.A.
GNU Taler is free software; you can redistribute it and/or modify it under the
terms of the GNU General Public License as published by the Free Software
Foundation; either version 3, or (at your option) any later version.
GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
A PARTICULAR PURPOSE. See the GNU General Public License for more details.
You should have received a copy of the GNU General Public License along with
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
/**
*
* @author Sebastian Javier Marchano (sebasjm)
*/
section.hero.is-hero-bar {
background-color: $hero-bar-background;
border-bottom: $light-border;
.hero-body {
padding: $default-padding;
.level-item {
&.is-hero-avatar-item {
margin-right: $default-padding;
}
> div > .level {
margin-bottom: $default-padding * .5;
}
.subtitle + p {
margin-top: $default-padding * .5;
}
}
.button {
&.is-hero-button {
background-color: rgba($white, .5);
font-weight: 300;
@include transition(background-color);
&:hover {
background-color: $white;
}
}
}
}
}

View File

@ -0,0 +1,51 @@
/*
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/>
*/
.lds-ring {
display: inline-block;
position: relative;
width: 80px;
height: 80px;
}
.lds-ring div {
box-sizing: border-box;
display: block;
position: absolute;
width: 64px;
height: 64px;
margin: 8px;
border: 8px solid black;
border-radius: 50%;
animation: lds-ring 1.2s cubic-bezier(0.5, 0, 0.5, 1) infinite;
border-color: black transparent transparent transparent;
}
.lds-ring div:nth-child(1) {
animation-delay: -0.45s;
}
.lds-ring div:nth-child(2) {
animation-delay: -0.3s;
}
.lds-ring div:nth-child(3) {
animation-delay: -0.15s;
}
@keyframes lds-ring {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}

View File

@ -0,0 +1,24 @@
/*
This file is part of GNU Taler
(C) 2021 Taler Systems S.A.
GNU Taler is free software; you can redistribute it and/or modify it under the
terms of the GNU General Public License as published by the Free Software
Foundation; either version 3, or (at your option) any later version.
GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
A PARTICULAR PURPOSE. See the GNU General Public License for more details.
You should have received a copy of the GNU General Public License along with
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
/**
*
* @author Sebastian Javier Marchano (sebasjm)
*/
section.section.is-main-section {
padding-top: $default-padding;
}

View File

@ -0,0 +1,50 @@
/*
This file is part of GNU Taler
(C) 2021 Taler Systems S.A.
GNU Taler is free software; you can redistribute it and/or modify it under the
terms of the GNU General Public License as published by the Free Software
Foundation; either version 3, or (at your option) any later version.
GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
A PARTICULAR PURPOSE. See the GNU General Public License for more details.
You should have received a copy of the GNU General Public License along with
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
/**
*
* @author Sebastian Javier Marchano (sebasjm)
*/
.is-user-avatar {
&.has-max-width {
max-width: $size-base * 7;
}
&.is-aligned-center {
margin: 0 auto;
}
img {
margin: 0 auto;
border-radius: $radius-rounded;
}
}
.icon.has-update-mark {
position: relative;
&:after {
content: "";
width: $icon-update-mark-size;
height: $icon-update-mark-size;
position: absolute;
top: 1px;
right: 1px;
background-color: $icon-update-mark-color;
border-radius: $radius-rounded;
}
}

View File

@ -0,0 +1,34 @@
/*
This file is part of GNU Taler
(C) 2021 Taler Systems S.A.
GNU Taler is free software; you can redistribute it and/or modify it under the
terms of the GNU General Public License as published by the Free Software
Foundation; either version 3, or (at your option) any later version.
GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
A PARTICULAR PURPOSE. See the GNU General Public License for more details.
You should have received a copy of the GNU General Public License along with
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
/**
*
* @author Sebastian Javier Marchano (sebasjm)
*/
@mixin transition($t) {
transition: $t 250ms ease-in-out 50ms;
}
@mixin icon-with-update-mark ($icon-base-width) {
.icon {
width: $icon-base-width;
&.has-update-mark:after {
right: ($icon-base-width / 2) - .85;
}
}
}

View File

@ -0,0 +1,35 @@
/*
This file is part of GNU Taler
(C) 2021 Taler Systems S.A.
GNU Taler is free software; you can redistribute it and/or modify it under the
terms of the GNU General Public License as published by the Free Software
Foundation; either version 3, or (at your option) any later version.
GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
A PARTICULAR PURPOSE. See the GNU General Public License for more details.
You should have received a copy of the GNU General Public License along with
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
/**
*
* @author Sebastian Javier Marchano (sebasjm)
*/
.modal-card {
width: $modal-card-width;
}
.modal-card-foot {
background-color: $modal-card-foot-background-color;
}
@include mobile {
.modal .animation-content .modal-card {
width: $modal-card-width-mobile;
margin: 0 auto;
}
}

View File

@ -0,0 +1,144 @@
/*
This file is part of GNU Taler
(C) 2021 Taler Systems S.A.
GNU Taler is free software; you can redistribute it and/or modify it under the
terms of the GNU General Public License as published by the Free Software
Foundation; either version 3, or (at your option) any later version.
GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
A PARTICULAR PURPOSE. See the GNU General Public License for more details.
You should have received a copy of the GNU General Public License along with
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
/**
*
* @author Sebastian Javier Marchano (sebasjm)
*/
nav.navbar {
box-shadow: $navbar-box-shadow;
.navbar-item {
&.has-user-avatar {
.is-user-avatar {
margin-right: $default-padding * .5;
display: inline-flex;
width: $navbar-avatar-size;
height: $navbar-avatar-size;
}
}
&.has-divider {
border-right: $navbar-divider-border;
}
&.no-left-space {
padding-left: 0;
}
&.has-dropdown {
padding-right: 0;
padding-left: 0;
.navbar-link {
padding-right: $navbar-item-h-padding;
padding-left: $navbar-item-h-padding;
}
}
&.has-control {
padding-top: 0;
padding-bottom: 0;
}
.control {
.input {
color: $navbar-input-color;
border: 0;
box-shadow: none;
background: transparent;
&::placeholder {
color: $navbar-input-placeholder-color;
}
}
}
}
}
@include touch {
nav.navbar {
display: flex;
padding-right: 0;
.navbar-brand {
flex: 1;
&.is-right {
flex: none;
}
}
.navbar-item {
&.no-left-space-touch {
padding-left: 0;
}
}
.navbar-menu {
position: absolute;
width: 100vw;
padding-top: 0;
top: $navbar-height;
left: 0;
.navbar-item {
.icon:first-child {
margin-right: $default-padding * .5;
}
&.has-dropdown {
>.navbar-link {
background-color: $white-ter;
.icon:last-child {
display: none;
}
}
}
&.has-user-avatar {
>.navbar-link {
display: flex;
align-items: center;
padding-top: $default-padding * .5;
padding-bottom: $default-padding * .5;
}
}
}
}
}
}
@include desktop {
nav.navbar {
.navbar-item {
padding-right: $navbar-item-h-padding;
padding-left: $navbar-item-h-padding;
&:not(.is-desktop-icon-only) {
.icon:first-child {
margin-right: $default-padding * .5;
}
}
&.is-desktop-icon-only {
span:not(.icon) {
display: none;
}
}
}
}
}

View File

@ -0,0 +1,173 @@
/*
This file is part of GNU Taler
(C) 2021 Taler Systems S.A.
GNU Taler is free software; you can redistribute it and/or modify it under the
terms of the GNU General Public License as published by the Free Software
Foundation; either version 3, or (at your option) any later version.
GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
A PARTICULAR PURPOSE. See the GNU General Public License for more details.
You should have received a copy of the GNU General Public License along with
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
/**
*
* @author Sebastian Javier Marchano (sebasjm)
*/
table.table {
thead {
th {
border-bottom-width: 1px;
}
}
td, th {
&.checkbox-cell {
.b-checkbox.checkbox:not(.button) {
margin-right: 0;
width: 20px;
.control-label {
display: none;
padding: 0;
}
}
}
}
td {
.image {
margin: 0 auto;
width: $table-avatar-size;
height: $table-avatar-size;
}
&.is-progress-col {
min-width: 5rem;
vertical-align: middle;
}
}
}
.b-table {
.table {
border: 0;
border-radius: 0;
}
/* This stylizes buefy's pagination */
.table-wrapper {
margin-bottom: 0;
}
.table-wrapper + .level {
padding: $notification-padding;
padding-left: $card-content-padding;
padding-right: $card-content-padding;
margin: 0;
border-top: $base-color-light;
background: $notification-background-color;
.pagination-link {
background: $button-background-color;
color: $button-color;
border-color: $button-border-color;
&.is-current {
border-color: $button-active-border-color;
}
}
.pagination-previous, .pagination-next, .pagination-link {
border-color: $button-border-color;
color: $base-color;
&[disabled] {
background-color: transparent;
}
}
}
}
@include mobile {
.card {
&.has-table {
.b-table {
.table-wrapper + .level {
.level-left + .level-right {
margin-top: 0;
}
}
}
}
&.has-mobile-sort-spaced {
.b-table {
.field.table-mobile-sort {
padding-top: $default-padding * .5;
}
}
}
}
.b-table {
.field.table-mobile-sort {
padding: 0 $default-padding * .5;
}
.table-wrapper.has-mobile-cards {
tr {
box-shadow: 0 2px 3px rgba(10, 10, 10, 0.1);
margin-bottom: 3px!important;
}
td {
&.is-progress-col {
span, progress {
display: flex;
width: 45%;
align-items: center;
align-self: center;
}
}
&.checkbox-cell, &.is-image-cell {
border-bottom: 0!important;
}
&.checkbox-cell, &.is-actions-cell {
&:before {
display: none;
}
}
&.has-no-head-mobile {
&:before {
display: none;
}
span {
display: block;
width: 100%;
}
&.is-progress-col {
progress {
width: 100%;
}
}
&.is-image-cell {
.image {
width: $table-avatar-size-mobile;
height: auto;
margin: 0 auto $default-padding * .25;
}
}
}
}
}
}
}

View File

@ -0,0 +1,136 @@
/*
This file is part of GNU Taler
(C) 2021 Taler Systems S.A.
GNU Taler is free software; you can redistribute it and/or modify it under the
terms of the GNU General Public License as published by the Free Software
Foundation; either version 3, or (at your option) any later version.
GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
A PARTICULAR PURPOSE. See the GNU General Public License for more details.
You should have received a copy of the GNU General Public License along with
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
/**
*
* @author Sebastian Javier Marchano (sebasjm)
*/
/* We'll need some initial vars to use here */
@import "node_modules/bulma/sass/utilities/initial-variables";
/* Base: Size */
$size-base: 1rem;
$default-padding: $size-base * 1.5;
/* Default font */
$family-sans-serif: "Nunito", sans-serif;
/* Base color */
$base-color: #2e323a;
$base-color-light: rgba(24, 28, 33, 0.06);
/* General overrides */
$primary: $turquoise;
$body-background-color: #f8f8f8;
$link: $blue;
$link-visited: $purple;
$light-border: 1px solid $base-color-light;
$hr-height: 1px;
/* NavBar: specifics */
$navbar-input-color: $grey-darker;
$navbar-input-placeholder-color: $grey-lighter;
$navbar-box-shadow: 0 1px 0 rgba(24, 28, 33, 0.04);
$navbar-divider-border: 1px solid rgba($grey-lighter, 0.25);
$navbar-item-h-padding: $default-padding * 0.75;
$navbar-avatar-size: 1.75rem;
/* Aside: Bulma override */
$menu-item-radius: 0;
$menu-list-link-padding: $size-base * 0.5 0;
$menu-label-color: lighten($base-color, 25%);
$menu-item-color: lighten($base-color, 30%);
$menu-item-hover-color: $white;
$menu-item-hover-background-color: darken($base-color, 3.5%);
$menu-item-active-color: $white;
$menu-item-active-background-color: darken($base-color, 2.5%);
/* Aside: specifics */
$aside-width: $size-base * 14;
$aside-mobile-width: $size-base * 15;
$aside-icon-width: $size-base * 3;
$aside-submenu-font-size: $size-base * 0.95;
$aside-box-shadow: none;
$aside-background-color: $base-color;
$aside-tools-background-color: darken($aside-background-color, 10%);
$aside-tools-color: $white;
/* Title Bar: specifics */
$title-bar-color: $grey;
$title-bar-active-color: $black-ter;
/* Hero Bar: specifics */
$hero-bar-background: $white;
/* Card: Bulma override */
$card-shadow: none;
$card-header-shadow: none;
/* Card: specifics */
$card-border: 1px solid $base-color-light;
$card-header-border-bottom-color: $base-color-light;
/* Table: Bulma override */
$table-cell-border: 1px solid $white-bis;
/* Table: specifics */
$table-avatar-size: $size-base * 1.5;
$table-avatar-size-mobile: 25vw;
/* Form */
$checkbox-border: 1px solid $base-color;
/* Modal card: Bulma override */
$modal-card-head-background-color: $white-ter;
$modal-card-title-size: $size-base;
$modal-card-body-padding: $default-padding 20px;
$modal-card-head-border-bottom: 1px solid $white-ter;
$modal-card-foot-border-top: 0;
/* Modal card: specifics */
$modal-card-width: 80vw;
$modal-card-width-mobile: 90vw;
$modal-card-foot-background-color: $white-ter;
/* Notification: Bulma override */
$notification-padding: $default-padding * 0.75 $default-padding;
/* Footer: Bulma override */
$footer-background-color: $white;
$footer-padding: $default-padding * 0.33 $default-padding;
/* Footer: specifics */
$footer-logo-height: $size-base * 2;
/* Progress: Bulma override */
$progress-bar-background-color: $grey-lighter;
/* Icon: specifics */
$icon-update-mark-size: $size-base * 0.5;
$icon-update-mark-color: $yellow;
$input-disabled-border-color: $grey-lighter;
$table-row-hover-background-color: hsl(0, 0%, 80%);
.menu-list {
div {
border-radius: $menu-item-radius;
color: $menu-item-color;
display: block;
padding: $menu-list-link-padding;
}
}

View File

@ -0,0 +1,25 @@
/*
This file is part of GNU Taler
(C) 2021 Taler Systems S.A.
GNU Taler is free software; you can redistribute it and/or modify it under the
terms of the GNU General Public License as published by the Free Software
Foundation; either version 3, or (at your option) any later version.
GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
A PARTICULAR PURPOSE. See the GNU General Public License for more details.
You should have received a copy of the GNU General Public License along with
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
/**
*
* @author Sebastian Javier Marchano (sebasjm)
*/
.is-tiles-wrapper {
margin-bottom: $default-padding;
}

View File

@ -0,0 +1,50 @@
/*
This file is part of GNU Taler
(C) 2021 Taler Systems S.A.
GNU Taler is free software; you can redistribute it and/or modify it under the
terms of the GNU General Public License as published by the Free Software
Foundation; either version 3, or (at your option) any later version.
GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
A PARTICULAR PURPOSE. See the GNU General Public License for more details.
You should have received a copy of the GNU General Public License along with
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
/**
*
* @author Sebastian Javier Marchano (sebasjm)
*/
section.section.is-title-bar {
padding: $default-padding;
border-bottom: $light-border;
ul {
li {
display: inline-block;
padding: 0 $default-padding * .5 0 0;
font-size: $default-padding;
color: $title-bar-color;
&:after {
display: inline-block;
content: '/';
padding-left: $default-padding * .5;
}
&:last-child {
padding-right: 0;
font-weight: 900;
color: $title-bar-active-color;
&:after {
display: none;
}
}
}
}
}

View File

@ -0,0 +1,22 @@
/*
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/>
*/
@font-face {
font-family: 'Nunito';
font-style: normal;
font-weight: 400;
src: url(./XRXV3I6Li01BKofINeaE.ttf) format('truetype');
}

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,29 @@
/*
This file is part of GNU Taler
(C) 2021 Taler Systems S.A.
GNU Taler is free software; you can redistribute it and/or modify it under the
terms of the GNU General Public License as published by the Free Software
Foundation; either version 3, or (at your option) any later version.
GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
A PARTICULAR PURPOSE. See the GNU General Public License for more details.
You should have received a copy of the GNU General Public License along with
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
/**
*
* @author Sebastian Javier Marchano (sebasjm)
*/
@import "node_modules/bulma-radio/bulma-radio";
// @import "node_modules/bulma-responsive-tables/bulma-responsive-tables";
@import "node_modules/bulma-checkbox/bulma-checkbox";
// @import "node_modules/bulma-switch-control/bulma-switch-control";
// @import "node_modules/bulma-upload-control/bulma-upload-control";
/* Bulma */
@import "node_modules/bulma/bulma";

View File

@ -0,0 +1,191 @@
/*
This file is part of GNU Taler
(C) 2021 Taler Systems S.A.
GNU Taler is free software; you can redistribute it and/or modify it under the
terms of the GNU General Public License as published by the Free Software
Foundation; either version 3, or (at your option) any later version.
GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
A PARTICULAR PURPOSE. See the GNU General Public License for more details.
You should have received a copy of the GNU General Public License along with
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
/**
*
* @author Sebastian Javier Marchano (sebasjm)
*/
/* Theme style (colors & sizes) */
@import "theme-default";
/* Core Libs & Lib configs */
@import "libs/all";
/* Mixins */
@import "mixins";
/* Theme components */
@import "nav-bar";
@import "aside";
@import "title-bar";
@import "hero-bar";
@import "card";
@import "table";
@import "tiles";
@import "form";
@import "main-section";
@import "modal";
@import "footer";
@import "misc";
@import "custom-calendar";
@import "loading";
@import "fonts/nunito.css";
@import "icons/materialdesignicons-4.9.95.min.css";
$tooltip-color: red;
@import "../../node_modules/@creativebulma/bulma-tooltip/dist/bulma-tooltip.min.css";
// @import "../../node_modules/bulma-timeline/dist/css/bulma-timeline.min.css";
.notification {
background-color: transparent;
}
.timeline .timeline-item .timeline-content {
padding-top: 0;
}
.timeline .timeline-item:last-child::before {
display: none;
}
.timeline .timeline-item .timeline-marker {
top: 0;
}
.toast {
position: absolute;
width: 60%;
margin-left: 10%;
margin-right: 10%;
z-index: 999;
display: flex;
flex-direction: column;
padding: 15px;
text-align: center;
pointer-events: none;
}
.toast > .message {
white-space: pre-wrap;
opacity: 80%;
}
div {
&.is-loading {
position: relative;
pointer-events: none;
opacity: 0.5;
&:after {
// @include loader;
position: absolute;
top: calc(50% - 2.5em);
left: calc(50% - 2.5em);
width: 5em;
height: 5em;
border-width: 0.25em;
}
}
}
input[type="checkbox"]:indeterminate + .check {
background: red !important;
}
.right-sticky {
position: sticky;
right: 0px;
background-color: $white;
}
.right-sticky .buttons {
flex-wrap: nowrap;
}
.table.is-striped tbody tr:not(.is-selected):nth-child(even) .right-sticky {
background-color: #fafafa;
}
tr:hover .right-sticky {
background-color: hsl(0, 0%, 80%);
}
.table.is-striped tbody tr:nth-child(even):hover .right-sticky {
background-color: hsl(0, 0%, 95%);
}
.content-full-size {
height: calc(100% - 3rem);
position: absolute;
width: calc(100% - 14rem);
display: flex;
}
.content-full-size .column .card {
min-width: 200px;
}
@include touch {
.content-full-size {
height: 100%;
position: absolute;
width: 100%;
}
}
.column.is-half {
flex: none;
width: 50%;
}
input:read-only {
cursor: initial;
}
[data-tooltip]:before {
max-width: 15rem;
width: max-content;
text-align: left;
transition: opacity 0.1s linear 1s;
// transform: inherit !important;
white-space: pre-wrap !important;
font-weight: normal;
// position: relative;
}
.icon[data-tooltip]:before {
transition: none;
z-index: 5;
}
span[data-tooltip] {
border-bottom: none;
}
div[data-tooltip]::before {
position: absolute;
}
.modal-card-body > p {
padding: 1em;
}
.modal-card-body > p.warning {
background-color: #fffbdd;
border: solid 1px #f2e9bf;
}

View File

@ -1,5 +1,5 @@
<!DOCTYPE html>
<html lang="en">
<html lang="en" class="has-aside-left has-aside-mobile-transition has-navbar-fixed-top has-aside-expanded">
<head>
<meta charset="utf-8">
<title><% preact.title %></title>

View File

@ -31,42 +31,56 @@ importers:
packages/anastasis-webui:
specifiers:
'@creativebulma/bulma-tooltip': ^1.2.0
'@gnu-taler/taler-util': workspace:^0.8.3
'@types/enzyme': ^3.10.5
'@types/jest': ^26.0.8
'@typescript-eslint/eslint-plugin': ^2.25.0
'@typescript-eslint/parser': ^2.25.0
anastasis-core: workspace:^0.0.1
bulma: ^0.9.3
bulma-checkbox: ^1.1.1
bulma-radio: ^1.1.1
enzyme: ^3.11.0
enzyme-adapter-preact-pure: ^3.1.0
eslint: ^6.8.0
eslint-config-preact: ^1.1.1
jed: 1.1.1
jest: ^26.2.2
jest-preset-preact: ^4.0.2
preact: ^10.3.1
preact-cli: ^3.2.2
preact-render-to-string: ^5.1.4
preact-router: ^3.2.1
sass: ^1.32.13
sass-loader: ^10.1.1
sirv-cli: ^1.0.0-next.3
typescript: ^3.7.5
dependencies:
'@gnu-taler/taler-util': link:../taler-util
anastasis-core: link:../anastasis-core
jed: 1.1.1
preact: 10.5.14
preact-render-to-string: 5.1.19_preact@10.5.14
preact-router: 3.2.1_preact@10.5.14
devDependencies:
'@creativebulma/bulma-tooltip': 1.2.0
'@types/enzyme': 3.10.9
'@types/jest': 26.0.24
'@typescript-eslint/eslint-plugin': 2.34.0_2b015b1c4b7c4a3ed9a197dc233b1a35
'@typescript-eslint/parser': 2.34.0_eslint@6.8.0+typescript@3.9.10
bulma: 0.9.3
bulma-checkbox: 1.1.1
bulma-radio: 1.1.1
enzyme: 3.11.0
enzyme-adapter-preact-pure: 3.1.0_enzyme@3.11.0+preact@10.5.14
eslint: 6.8.0
eslint-config-preact: 1.1.4_eslint@6.8.0+typescript@3.9.10
jest: 26.6.3
jest-preset-preact: 4.0.2_9b3f24ae35a87c3c82fffbe3fdf70e1e
preact-cli: 3.2.2_517d24bd855b57d7e424aceed04e063b
preact-cli: 3.2.2_8d1b4ee21ca5a56b4aabd4a3e659b2d7
sass: 1.43.2
sass-loader: 10.2.0_sass@1.43.2
sirv-cli: 1.0.14
typescript: 3.9.10
@ -3570,6 +3584,10 @@ packages:
arrify: 1.0.1
dev: true
/@creativebulma/bulma-tooltip/1.2.0:
resolution: {integrity: sha512-ooImbeXEBxf77cttbzA7X5rC5aAWm9UsXIGViFOnsqB+6M944GkB28S5R4UWRqjFd2iW4zGEkEifAU+q43pt2w==}
dev: true
/@emotion/cache/10.0.29:
resolution: {integrity: sha512-fU2VtSVlHiF27empSbxi1O2JFdNWZO+2NFHfwO0pxgTep6Xa3uGb+3pVKfLww2l/IBGLNEZl5Xf/++A4wAYDYQ==}
dependencies:
@ -4607,7 +4625,7 @@ packages:
dependencies:
'@types/estree': 0.0.39
estree-walker: 1.0.1
picomatch: 2.3.0
picomatch: 2.2.2
rollup: 2.56.2
dev: true
@ -8365,6 +8383,22 @@ packages:
resolution: {integrity: sha1-y5T662HIaWRR2zZTThQi+U8K7og=}
dev: true
/bulma-checkbox/1.1.1:
resolution: {integrity: sha512-16aTRbXQBCdfk8nrWSVJCasD28FudeVF+G+mZfMJc2N/xTcU4XXjzQ6Iya1neKOgXkXQMx9nJOH2n8H7LRztNg==}
dependencies:
bulma: 0.9.3
dev: true
/bulma-radio/1.1.1:
resolution: {integrity: sha512-aIHuMbpBGyZYx8KxbQRdjIy/0M9WHWz5VyxMggwxmCadnN0gd7gC/G96WUy9mhaoIfo9yX/Cf8pKQNinKH+w7w==}
dependencies:
bulma: 0.9.3
dev: true
/bulma/0.9.3:
resolution: {integrity: sha512-0d7GNW1PY4ud8TWxdNcP6Cc8Bu7MxcntD/RRLGWuiw/s0a9P+XlH/6QoOIrmbj6o8WWJzJYhytiu9nFjTszk1g==}
dev: true
/bytes/3.0.0:
resolution: {integrity: sha1-0ygVQE1olpn4Wk6k+odV3ROpYEg=}
engines: {node: '>= 0.8'}
@ -10784,18 +10818,18 @@ packages:
peerDependencies:
eslint: ^3 || ^4 || ^5 || ^6 || ^7
dependencies:
array-includes: 3.1.3
array-includes: 3.1.2
array.prototype.flatmap: 1.2.4
doctrine: 2.1.0
eslint: 6.8.0
has: 1.0.3
jsx-ast-utils: 3.2.0
object.entries: 1.1.4
object.fromentries: 2.0.4
object.values: 1.1.4
object.entries: 1.1.3
object.fromentries: 2.0.3
object.values: 1.1.2
prop-types: 15.7.2
resolve: 1.20.0
string.prototype.matchall: 4.0.5
resolve: 1.19.0
string.prototype.matchall: 4.0.3
dev: true
/eslint-plugin-react/7.22.0_eslint@7.18.0:
@ -16852,6 +16886,116 @@ packages:
- webpack-command
dev: true
/preact-cli/3.2.2_8d1b4ee21ca5a56b4aabd4a3e659b2d7:
resolution: {integrity: sha512-42aUanAb/AqHHvnfb/IwJw9UhY5iuHkGRBv3TrTsQMrq0Ee8Z84r+HS8wjGI0aHHb0R8tnHI0hhllWgmNhjB/Q==}
engines: {node: '>=12'}
hasBin: true
peerDependencies:
less-loader: ^7.3.0
preact: '*'
preact-render-to-string: '*'
sass-loader: ^10.2.0
stylus-loader: ^4.3.3
peerDependenciesMeta:
less-loader:
optional: true
sass-loader:
optional: true
stylus-loader:
optional: true
dependencies:
'@babel/core': 7.15.0
'@babel/plugin-proposal-class-properties': 7.14.5_@babel+core@7.15.0
'@babel/plugin-proposal-decorators': 7.14.5_@babel+core@7.15.0
'@babel/plugin-proposal-object-rest-spread': 7.14.7_@babel+core@7.15.0
'@babel/plugin-syntax-dynamic-import': 7.8.3_@babel+core@7.15.0
'@babel/plugin-transform-object-assign': 7.14.5_@babel+core@7.15.0
'@babel/plugin-transform-react-jsx': 7.14.9_@babel+core@7.15.0
'@babel/preset-env': 7.15.0_@babel+core@7.15.0
'@babel/preset-typescript': 7.15.0_@babel+core@7.15.0
'@preact/async-loader': 3.0.1_preact@10.5.14
'@prefresh/babel-plugin': 0.4.1
'@prefresh/webpack': 3.3.2_b4d84c08f02729896cbfdece19209372
autoprefixer: 10.3.1_postcss@8.3.6
babel-esm-plugin: 0.9.0_webpack@4.46.0
babel-loader: 8.2.2_be352a5a80662835a7707f972edfcfde
babel-plugin-macros: 3.1.0
babel-plugin-transform-react-remove-prop-types: 0.4.24
browserlist: 1.0.1
browserslist: 4.16.8
compression-webpack-plugin: 6.1.1_webpack@4.46.0
console-clear: 1.1.1
copy-webpack-plugin: 6.4.1_webpack@4.46.0
critters-webpack-plugin: 2.5.0
cross-spawn-promise: 0.10.2
css-loader: 5.2.7_webpack@4.46.0
ejs-loader: 0.5.0
envinfo: 7.8.1
esm: 3.2.25
fast-async: 6.3.8
file-loader: 6.2.0_webpack@4.46.0
fork-ts-checker-webpack-plugin: 4.1.6
get-port: 5.1.1
gittar: 0.1.1
glob: 7.1.7
html-webpack-exclude-assets-plugin: 0.0.7
html-webpack-plugin: 3.2.0_webpack@4.46.0
ip: 1.1.5
isomorphic-unfetch: 3.1.0
kleur: 4.1.4
loader-utils: 2.0.0
mini-css-extract-plugin: 1.6.2_webpack@4.46.0
minimatch: 3.0.4
native-url: 0.3.4
optimize-css-assets-webpack-plugin: 6.0.1_webpack@4.46.0
ora: 5.4.1
pnp-webpack-plugin: 1.7.0_typescript@4.4.3
postcss: 8.3.6
postcss-load-config: 3.1.0
postcss-loader: 4.3.0_postcss@8.3.6+webpack@4.46.0
preact: 10.5.14
preact-render-to-string: 5.1.19_preact@10.5.14
progress-bar-webpack-plugin: 2.1.0_webpack@4.46.0
promise-polyfill: 8.2.0
prompts: 2.4.1
raw-loader: 4.0.2_webpack@4.46.0
react-refresh: 0.10.0
rimraf: 3.0.2
sade: 1.7.4
sass-loader: 10.2.0_sass@1.43.2
size-plugin: 3.0.0_webpack@4.46.0
source-map: 0.7.3
stack-trace: 0.0.10
style-loader: 2.0.0_webpack@4.46.0
terser-webpack-plugin: 4.2.3_webpack@4.46.0
typescript: 4.4.3
update-notifier: 5.1.0
url-loader: 4.1.1_file-loader@6.2.0+webpack@4.46.0
validate-npm-package-name: 3.0.0
webpack: 4.46.0
webpack-bundle-analyzer: 4.4.2
webpack-dev-server: 3.11.2_webpack@4.46.0
webpack-fix-style-only-entries: 0.6.1
webpack-merge: 5.8.0
webpack-plugin-replace: 1.2.0
which: 2.0.2
workbox-cacheable-response: 6.2.4
workbox-core: 6.2.4
workbox-precaching: 6.2.4
workbox-routing: 6.2.4
workbox-strategies: 6.2.4
workbox-webpack-plugin: 6.2.4_webpack@4.46.0
transitivePeerDependencies:
- '@types/babel__core'
- bufferutil
- debug
- supports-color
- ts-node
- utf-8-validate
- webpack-cli
- webpack-command
dev: true
/preact-render-to-string/5.1.19_preact@10.5.14:
resolution: {integrity: sha512-bj8sn/oytIKO6RtOGSS/1+5CrQyRSC99eLUnEVbqUa6MzJX5dYh7wu9bmT0d6lm/Vea21k9KhCQwvr2sYN3rrQ==}
peerDependencies:
@ -18075,11 +18219,11 @@ packages:
peerDependencies:
rollup: ^2.0.0
dependencies:
'@babel/code-frame': 7.14.5
'@babel/code-frame': 7.12.13
jest-worker: 26.6.2
rollup: 2.56.2
serialize-javascript: 4.0.0
terser: 5.7.1
terser: 5.4.0
dev: true
/rollup/2.37.1:
@ -18188,6 +18332,38 @@ packages:
walker: 1.0.7
dev: true
/sass-loader/10.2.0_sass@1.43.2:
resolution: {integrity: sha512-kUceLzC1gIHz0zNJPpqRsJyisWatGYNFRmv2CKZK2/ngMJgLqxTbXwe/hJ85luyvZkgqU3VlJ33UVF2T/0g6mw==}
engines: {node: '>= 10.13.0'}
peerDependencies:
fibers: '>= 3.1.0'
node-sass: ^4.0.0 || ^5.0.0 || ^6.0.0
sass: ^1.3.0
webpack: ^4.36.0 || ^5.0.0
peerDependenciesMeta:
fibers:
optional: true
node-sass:
optional: true
sass:
optional: true
dependencies:
klona: 2.0.4
loader-utils: 2.0.0
neo-async: 2.6.2
sass: 1.43.2
schema-utils: 3.1.1
semver: 7.3.5
dev: true
/sass/1.43.2:
resolution: {integrity: sha512-DncYhjl3wBaPMMJR0kIUaH3sF536rVrOcqqVGmTZHQRRzj7LQlyGV7Mb8aCKFyILMr5VsPHwRYtyKpnKYlmQSQ==}
engines: {node: '>=8.9.0'}
hasBin: true
dependencies:
chokidar: 3.5.2
dev: true
/sax/1.2.4:
resolution: {integrity: sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==}
dev: true