anastasis-webui: ui tweaks

This commit is contained in:
Florian Dold 2021-11-08 16:51:23 +01:00
parent 16662b194d
commit 6a0c5263bb
No known key found for this signature in database
GPG Key ID: D2E4F00F29D02A4B
5 changed files with 190 additions and 87 deletions

View File

@ -109,13 +109,23 @@ export * from "./challenge-feedback-types.js";
const logger = new Logger("anastasis-core:index.ts"); const logger = new Logger("anastasis-core:index.ts");
function getContinents(): ContinentInfo[] { function getContinents(
opts: { requireProvider?: boolean } = {},
): ContinentInfo[] {
const currenciesWithProvider = new Set<string>();
anastasisData.providersList.anastasis_provider.forEach((x) => {
currenciesWithProvider.add(x.currency);
});
const continentSet = new Set<string>(); const continentSet = new Set<string>();
const continents: ContinentInfo[] = []; const continents: ContinentInfo[] = [];
for (const country of anastasisData.countriesList.countries) { for (const country of anastasisData.countriesList.countries) {
if (continentSet.has(country.continent)) { if (continentSet.has(country.continent)) {
continue; continue;
} }
if (opts.requireProvider && !currenciesWithProvider.has(country.currency)) {
// Country's currency doesn't have any providers => skip
continue;
}
continentSet.add(country.continent); continentSet.add(country.continent);
continents.push({ continents.push({
...{ name_i18n: country.continent_i18n }, ...{ name_i18n: country.continent_i18n },
@ -148,9 +158,18 @@ export class ReducerError extends Error {
* Get countries for a continent, abort with ReducerError * Get countries for a continent, abort with ReducerError
* exception when continent doesn't exist. * exception when continent doesn't exist.
*/ */
function getCountries(continent: string): CountryInfo[] { function getCountries(
continent: string,
opts: { requireProvider?: boolean } = {},
): CountryInfo[] {
const currenciesWithProvider = new Set<string>();
anastasisData.providersList.anastasis_provider.forEach((x) => {
currenciesWithProvider.add(x.currency);
});
const countries = anastasisData.countriesList.countries.filter( const countries = anastasisData.countriesList.countries.filter(
(x) => x.continent === continent, (x) =>
x.continent === continent &&
(!opts.requireProvider || currenciesWithProvider.has(x.currency)),
); );
if (countries.length <= 0) { if (countries.length <= 0) {
throw new ReducerError({ throw new ReducerError({
@ -164,14 +183,18 @@ function getCountries(continent: string): CountryInfo[] {
export async function getBackupStartState(): Promise<ReducerStateBackup> { export async function getBackupStartState(): Promise<ReducerStateBackup> {
return { return {
backup_state: BackupStates.ContinentSelecting, backup_state: BackupStates.ContinentSelecting,
continents: getContinents(), continents: getContinents({
requireProvider: true,
}),
}; };
} }
export async function getRecoveryStartState(): Promise<ReducerStateRecovery> { export async function getRecoveryStartState(): Promise<ReducerStateRecovery> {
return { return {
recovery_state: RecoveryStates.ContinentSelecting, recovery_state: RecoveryStates.ContinentSelecting,
continents: getContinents(), continents: getContinents({
requireProvider: true,
}),
}; };
} }
@ -1073,7 +1096,9 @@ async function backupSelectContinent(
state: ReducerStateBackup, state: ReducerStateBackup,
args: ActionArgsSelectContinent, args: ActionArgsSelectContinent,
): Promise<ReducerStateBackup | ReducerStateError> { ): Promise<ReducerStateBackup | ReducerStateError> {
const countries = getCountries(args.continent); const countries = getCountries(args.continent, {
requireProvider: true,
});
if (countries.length <= 0) { if (countries.length <= 0) {
return { return {
code: TalerErrorCode.ANASTASIS_REDUCER_INPUT_INVALID, code: TalerErrorCode.ANASTASIS_REDUCER_INPUT_INVALID,
@ -1092,7 +1117,9 @@ async function recoverySelectContinent(
state: ReducerStateRecovery, state: ReducerStateRecovery,
args: ActionArgsSelectContinent, args: ActionArgsSelectContinent,
): Promise<ReducerStateRecovery | ReducerStateError> { ): Promise<ReducerStateRecovery | ReducerStateError> {
const countries = getCountries(args.continent); const countries = getCountries(args.continent, {
requireProvider: true,
});
return { return {
...state, ...state,
recovery_state: RecoveryStates.CountrySelecting, recovery_state: RecoveryStates.CountrySelecting,

View File

@ -19,9 +19,9 @@
* @author Sebastian Javier Marchano (sebasjm) * @author Sebastian Javier Marchano (sebasjm)
*/ */
import { h, VNode } from 'preact'; import { h, VNode } from "preact";
import logo from '../../assets/logo.jpeg'; import logo from "../../assets/logo.jpeg";
import { LangSelector } from './LangSelector'; import { LangSelector } from "./LangSelector";
interface Props { interface Props {
onMobileMenu: () => void; onMobileMenu: () => void;
@ -29,14 +29,38 @@ interface Props {
} }
export function NavigationBar({ onMobileMenu, title }: Props): VNode { export function NavigationBar({ onMobileMenu, title }: Props): VNode {
return (<nav class="navbar is-fixed-top" role="navigation" aria-label="main navigation"> return (
<nav
class="navbar is-fixed-top"
role="navigation"
aria-label="main navigation"
>
<div class="navbar-brand"> <div class="navbar-brand">
<span class="navbar-item" style={{ fontSize: 24, fontWeight: 900 }}>{title}</span> <span class="navbar-item" style={{ fontSize: 24, fontWeight: 900 }}>
{title}
<a role="button" class="navbar-burger" aria-label="menu" aria-expanded="false" onClick={(e) => { </span>
onMobileMenu() <a
e.stopPropagation() href="mailto:contact@anastasis.lu"
}}> style={{ alignSelf: "center", padding: "0.5em" }}
>
Contact us
</a>
<a
href="https://bugs.anastasis.li/"
style={{ alignSelf: "center", padding: "0.5em" }}
>
Report a bug
</a>
<a
role="button"
class="navbar-burger"
aria-label="menu"
aria-expanded="false"
onClick={(e) => {
onMobileMenu();
e.stopPropagation();
}}
>
<span aria-hidden="true" /> <span aria-hidden="true" />
<span aria-hidden="true" /> <span aria-hidden="true" />
<span aria-hidden="true" /> <span aria-hidden="true" />

View File

@ -44,9 +44,9 @@ export function Sidebar({ mobile }: Props): VNode {
</div>} */} </div>} */}
<div class="aside-tools"> <div class="aside-tools">
<div class="aside-tools-label"> <div class="aside-tools-label">
<div><b>Anastasis</b> Reducer</div> <div><b>Anastasis</b></div>
<div class="is-size-7 has-text-right" style={{ lineHeight: 0, marginTop: -10 }}> <div class="is-size-7 has-text-right" style={{ lineHeight: 0, marginTop: -10 }}>
{process.env.__VERSION__} ({config.version}) Version {process.env.__VERSION__} ({config.version})
</div> </div>
</div> </div>
</div> </div>

View File

@ -15,41 +15,53 @@
*/ */
import { ComponentChildren, Fragment, h, VNode } from "preact"; import { ComponentChildren, Fragment, h, VNode } from "preact";
import Match from 'preact-router/match'; import Match from "preact-router/match";
import { useEffect, useState } from "preact/hooks"; import { useEffect, useState } from "preact/hooks";
import { NavigationBar } from "./NavigationBar"; import { NavigationBar } from "./NavigationBar";
import { Sidebar } from "./SideBar"; import { Sidebar } from "./SideBar";
interface MenuProps { interface MenuProps {
title: string; title: string;
} }
function WithTitle({ title, children }: { title: string; children: ComponentChildren }): VNode { function WithTitle({
title,
children,
}: {
title: string;
children: ComponentChildren;
}): VNode {
useEffect(() => { useEffect(() => {
document.title = `Taler Backoffice: ${title}` document.title = `${title}`;
}, [title]) }, [title]);
return <Fragment>{children}</Fragment> return <Fragment>{children}</Fragment>;
} }
export function Menu({ title }: MenuProps): VNode { export function Menu({ title }: MenuProps): VNode {
const [mobileOpen, setMobileOpen] = useState(false) const [mobileOpen, setMobileOpen] = useState(false);
return <Match>{({ path }: { path: string }) => { return (
const titleWithSubtitle = title // title ? title : (!admin ? getInstanceTitle(path, instance) : getAdminTitle(path, instance)) <Match>
return (<WithTitle title={titleWithSubtitle}> {({ path }: { path: string }) => {
<div class={mobileOpen ? "has-aside-mobile-expanded" : ""} onClick={() => setMobileOpen(false)}> const titleWithSubtitle = title; // title ? title : (!admin ? getInstanceTitle(path, instance) : getAdminTitle(path, instance))
<NavigationBar onMobileMenu={() => setMobileOpen(!mobileOpen)} title={titleWithSubtitle} /> return (
<WithTitle title={titleWithSubtitle}>
<div
class={mobileOpen ? "has-aside-mobile-expanded" : ""}
onClick={() => setMobileOpen(false)}
>
<NavigationBar
onMobileMenu={() => setMobileOpen(!mobileOpen)}
title={titleWithSubtitle}
/>
<Sidebar mobile={mobileOpen} /> <Sidebar mobile={mobileOpen} />
</div> </div>
</WithTitle> </WithTitle>
) );
}}</Match> }}
</Match>
);
} }
interface NotYetReadyAppMenuProps { interface NotYetReadyAppMenuProps {
@ -60,37 +72,56 @@ interface NotYetReadyAppMenuProps {
interface NotifProps { interface NotifProps {
notification?: Notification; notification?: Notification;
} }
export function NotificationCard({ notification: n }: NotifProps): VNode | null { export function NotificationCard({
if (!n) return null notification: n,
return <div class="notification"> }: NotifProps): VNode | null {
if (!n) return null;
return (
<div class="notification">
<div class="columns is-vcentered"> <div class="columns is-vcentered">
<div class="column is-12"> <div class="column is-12">
<article class={n.type === 'ERROR' ? "message is-danger" : (n.type === 'WARN' ? "message is-warning" : "message is-info")}> <article
class={
n.type === "ERROR"
? "message is-danger"
: n.type === "WARN"
? "message is-warning"
: "message is-info"
}
>
<div class="message-header"> <div class="message-header">
<p>{n.message}</p> <p>{n.message}</p>
</div> </div>
{n.description && {n.description && <div class="message-body">{n.description}</div>}
<div class="message-body">
{n.description}
</div>}
</article> </article>
</div> </div>
</div> </div>
</div> </div>
);
} }
export function NotYetReadyAppMenu({ onLogout, title }: NotYetReadyAppMenuProps): VNode { export function NotYetReadyAppMenu({
const [mobileOpen, setMobileOpen] = useState(false) onLogout,
title,
}: NotYetReadyAppMenuProps): VNode {
const [mobileOpen, setMobileOpen] = useState(false);
useEffect(() => { useEffect(() => {
document.title = `Taler Backoffice: ${title}` document.title = `Taler Backoffice: ${title}`;
}, [title]) }, [title]);
return <div class={mobileOpen ? "has-aside-mobile-expanded" : ""} onClick={() => setMobileOpen(false)}> return (
<NavigationBar onMobileMenu={() => setMobileOpen(!mobileOpen)} title={title} /> <div
class={mobileOpen ? "has-aside-mobile-expanded" : ""}
onClick={() => setMobileOpen(false)}
>
<NavigationBar
onMobileMenu={() => setMobileOpen(!mobileOpen)}
title={title}
/>
{onLogout && <Sidebar mobile={mobileOpen} />} {onLogout && <Sidebar mobile={mobileOpen} />}
</div> </div>
);
} }
export interface Notification { export interface Notification {
@ -99,6 +130,5 @@ export interface Notification {
type: MessageType; type: MessageType;
} }
export type ValueOrFunction<T> = T | ((p: T) => T) export type ValueOrFunction<T> = T | ((p: T) => T);
export type MessageType = 'INFO' | 'WARN' | 'ERROR' | 'SUCCESS' export type MessageType = "INFO" | "WARN" | "ERROR" | "SUCCESS";

View File

@ -123,13 +123,35 @@ export function ContinentSelectionScreen(): VNode {
</div> </div>
<div class="column is-two-third"> <div class="column is-two-third">
<p> <p>
Your location will help us to determine which personal information Your choice will help us with asking the right information to unique
to ask you for the next step. identify you when you want to recover your backed up secrets.
</p> </p>
<p> <p>
You should choose the country that issued most of your long-term Choose the country that issued most of your long-term legal
legal documents or personal identifiers. documents or personal identifiers.
</p> </p>
<div
style={{
border: "1px solid gray",
borderRadius: "0.5em",
backgroundColor: "#fbfcbd",
padding: "0.5em",
}}
>
<p>
If you just want to try out Anastasis, we recomment that you
choose <b>Testcontinent</b> with <b>Demoland</b>. For this special
country, you will be asked for a simple number and not real,
personal identifiable information.
</p>
{/*
<p>
Because of the diversity of personally identifying information in
different countries and cultures, we do not support all countries
yet. If you want to improve the supported countries,{" "}
<a href="mailto:contact@anastasis.lu">contact us</a>.
</p> */}
</div>
</div> </div>
</div> </div>
</AnastasisClientFrame> </AnastasisClientFrame>