[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]
[taler-merchant-backoffice] 05/05: duration picker
From: |
gnunet |
Subject: |
[taler-merchant-backoffice] 05/05: duration picker |
Date: |
Thu, 24 Jun 2021 14:30:14 +0200 |
This is an automated email from the git hooks/post-receive script.
sebasjm pushed a commit to branch master
in repository merchant-backoffice.
commit 61f845b3dcd71b74a802b5e791ec1ea0d3c03c87
Author: Sebastian <sebasjm@gmail.com>
AuthorDate: Thu Jun 24 09:22:57 2021 -0300
duration picker
---
.../src/components/form/DurationPicker.scss | 71 ++++++++++
.../src/components/form/DurationPicker.stories.tsx | 50 +++++++
.../src/components/form/DurationPicker.tsx | 154 +++++++++++++++++++++
.../frontend/src/components/form/InputDuration.tsx | 104 +++++++++++---
.../instance/DefaultInstanceFormFields.tsx | 1 +
packages/frontend/src/components/modal/index.tsx | 12 ++
.../paths/instance/orders/create/CreatePage.tsx | 19 +--
.../src/paths/instance/orders/create/index.tsx | 2 +-
packages/frontend/src/utils/constants.ts | 2 +-
9 files changed, 385 insertions(+), 30 deletions(-)
diff --git a/packages/frontend/src/components/form/DurationPicker.scss
b/packages/frontend/src/components/form/DurationPicker.scss
new file mode 100644
index 0000000..a355753
--- /dev/null
+++ b/packages/frontend/src/components/form/DurationPicker.scss
@@ -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;
+}
diff --git a/packages/frontend/src/components/form/DurationPicker.stories.tsx
b/packages/frontend/src/components/form/DurationPicker.stories.tsx
new file mode 100644
index 0000000..275c80f
--- /dev/null
+++ b/packages/frontend/src/components/form/DurationPicker.stories.tsx
@@ -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)
+*/
+
+import { h, FunctionalComponent } from 'preact';
+import { useState } from 'preact/hooks';
+import { DurationPicker as TestedComponent } from './DurationPicker';
+
+
+export default {
+ title: 'Components/Picker/Duration',
+ component: TestedComponent,
+ argTypes: {
+ onCreate: { action: 'onCreate' },
+ goBack: { action: 'goBack' },
+ }
+};
+
+function createExample<Props>(Component: FunctionalComponent<Props>, props:
Partial<Props>) {
+ const r = (args: any) => <Component {...args} />
+ r.args = props
+ return r
+}
+
+export const Example = createExample(TestedComponent, {
+ days: true, minutes: true, hours: true, seconds: true,
+ value: 10000000
+});
+
+export const WithState = () => {
+ const [v,s] = useState<number>(1000000)
+ return <TestedComponent value={v} onChange={s} days minutes hours seconds />
+}
diff --git a/packages/frontend/src/components/form/DurationPicker.tsx
b/packages/frontend/src/components/form/DurationPicker.tsx
new file mode 100644
index 0000000..2d51f2d
--- /dev/null
+++ b/packages/frontend/src/components/form/DurationPicker.tsx
@@ -0,0 +1,154 @@
+/*
+ This file is part of GNU Taler
+ (C) 2021 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+*
+* @author Sebastian Javier Marchano (sebasjm)
+*/
+
+import { h, VNode } from "preact";
+import { useState } from "preact/hooks";
+import { useTranslator } from "../../i18n";
+import './DurationPicker.scss'
+
+export interface Props {
+ hours?: boolean;
+ minutes?: boolean;
+ seconds?: boolean;
+ days?: boolean;
+ onChange: (value: number) => void;
+ value: number
+}
+
+// inspiration taken from https://github.com/flurmbo/react-duration-picker
+export function DurationPicker({ days, hours, minutes, seconds, onChange,
value }: Props): VNode {
+ const s = 1000
+ const m = s * 60
+ const h = m * 60
+ const d = h * 24
+ const i18n = useTranslator()
+
+ return <div class="rdp-picker">
+ {days && <DurationColumn unit={i18n`days`} max={99}
+ value={Math.floor(value / d)}
+ onDecrease={value >= d ? () => onChange(value - d) : undefined}
+ onIncrease={value < 99 * d ? () => onChange(value + d) : undefined}
+ onChange={diff => onChange(value + diff * d)}
+ />}
+ {hours && <DurationColumn unit={i18n`hours`} max={23} min={1}
+ value={Math.floor(value / h) % 24}
+ onDecrease={value >= h ? () => onChange(value - h) : undefined}
+ onIncrease={value < 99 * d ? () => onChange(value + h) : undefined}
+ onChange={diff => onChange(value + diff * h)}
+ />}
+ {minutes && <DurationColumn unit={i18n`minutes`} max={59} min={1}
+ value={Math.floor(value / m) % 60}
+ onDecrease={value >= m ? () => onChange(value - m) : undefined}
+ onIncrease={value < 99 * d ? () => onChange(value + m) : undefined}
+ onChange={diff => onChange(value + diff * m)}
+ />}
+ {seconds && <DurationColumn unit={i18n`seconds`} max={59}
+ value={Math.floor(value / s) % 60}
+ onDecrease={value >= s ? () => onChange(value - s) : undefined}
+ onIncrease={value < 99 * d ? () => onChange(value + s) : undefined}
+ onChange={diff => onChange(value + diff * s)}
+ />}
+ </div>
+}
+
+interface ColProps {
+ unit: string,
+ min?: number,
+ max: number,
+ value: number,
+ onIncrease?: () => void;
+ onDecrease?: () => void;
+ onChange?: (diff: number) => void;
+}
+
+function InputNumber({ initial, onChange }: { initial: number, onChange: (n:
number) => void }) {
+ const [value, handler] = useState<{v:string}>({
+ v: toTwoDigitString(initial)
+ })
+
+ return <input
+ value={value.v}
+ onBlur={(e) => onChange(parseInt(value.v, 10))}
+ onInput={(e) => {
+ e.preventDefault()
+ const n = Number.parseInt(e.currentTarget.value, 10);
+ if (isNaN(n)) return handler({v:toTwoDigitString(initial)})
+ return handler({v:toTwoDigitString(n)})
+ }}
+ style={{ width: 50, border: 'none', fontSize: 'inherit', background:
'inherit' }} />
+}
+
+function DurationColumn({ unit, min = 0, max, value, onIncrease, onDecrease,
onChange }: ColProps): VNode {
+
+ const cellHeight = 35
+ return (
+ <div class="rdp-column-container">
+ <div class="rdp-masked-div">
+ <hr class="rdp-reticule" style={{ top: cellHeight * 2 - 1 }} />
+ <hr class="rdp-reticule" style={{ top: cellHeight * 3 - 1 }} />
+
+ <div class="rdp-column" style={{ top: 0 }}>
+
+ <div class="rdp-cell" key={value - 1}>
+ {onDecrease && <button style={{ width: '100%', textAlign:
'center', margin: 5 }}
+ onClick={onDecrease}>
+ <span class="icon">
+ <i class="mdi mdi-chevron-up" />
+ </span>
+ </button>}
+ </div>
+ <div class="rdp-cell" key={value - 1}>
+ {value > min ? toTwoDigitString(value - 1) : ''}
+ </div>
+ <div class="rdp-cell rdp-center" key={value}>
+ {onChange ?
+ <InputNumber initial={value} onChange={(n) => onChange(n -
value)} /> :
+ toTwoDigitString(value)
+ }
+ <div>{unit}</div>
+ </div>
+
+ <div class="rdp-cell" key={value + 1}>
+ {value < max ? toTwoDigitString(value + 1) : ''}
+ </div>
+
+ <div class="rdp-cell" key={value - 1}>
+ {onIncrease && <button style={{ width: '100%', textAlign:
'center', margin: 5 }}
+ onClick={onIncrease}>
+ <span class="icon">
+ <i class="mdi mdi-chevron-down" />
+ </span>
+ </button>}
+ </div>
+
+ </div>
+ </div>
+ </div>
+ );
+}
+
+
+function toTwoDigitString(n: number) {
+ if (n < 10) {
+ return `0${n}`;
+ }
+ return `${n}`;
+}
\ No newline at end of file
diff --git a/packages/frontend/src/components/form/InputDuration.tsx
b/packages/frontend/src/components/form/InputDuration.tsx
index 30afd65..76e9022 100644
--- a/packages/frontend/src/components/form/InputDuration.tsx
+++ b/packages/frontend/src/components/form/InputDuration.tsx
@@ -18,33 +18,99 @@
*
* @author Sebastian Javier Marchano (sebasjm)
*/
-import { formatDuration, intervalToDuration } from "date-fns";
+import { intervalToDuration, formatDuration } from "date-fns";
import { h, VNode } from "preact";
-import { RelativeTime } from "../../declaration";
+import { useState } from "preact/hooks";
+import { Translate, useTranslator } from "../../i18n";
+import { SimpleModal } from "../modal";
+import { DurationPicker } from "./DurationPicker";
import { InputProps, useField } from "./useField";
-import { InputWithAddon } from "./InputWithAddon";
export interface Props<T> extends InputProps<T> {
expand?: boolean;
readonly?: boolean;
+ withForever?: boolean;
}
-export function InputDuration<T>({ name, expand, placeholder, tooltip, label,
help, readonly }: Props<keyof T>): VNode {
- const { value } = useField<T>(name);
- return <InputWithAddon<T> name={name} readonly={readonly}
addonAfter={readableDuration(value as any)}
- expand={expand}
- label={label} placeholder={placeholder} help={help} tooltip={tooltip}
- toStr={(v?: RelativeTime) => `${(v && v.d_ms !== "forever" && v.d_ms ?
v.d_ms : '')}`}
- fromStr={(v: string) => ({ d_ms: (parseInt(v, 10)) || undefined })}
- />
-}
+export function InputDuration<T>({ name, expand, placeholder, tooltip, label,
help, readonly, withForever }: Props<keyof T>): VNode {
+ const [opened, setOpened] = useState(false)
+ const i18n = useTranslator()
-function readableDuration(duration?: RelativeTime): string {
- if (!duration) return ""
- if (duration.d_ms === "forever") return "forever"
- try {
- return formatDuration(intervalToDuration({ start: 0, end: duration.d_ms }))
- } catch (e) {
- return ''
+ const { error, required, value, onChange } = useField<T>(name);
+ let strValue = ''
+ if (!value) {
+ strValue = ''
+ } else if (value.d_ms === 'forever') {
+ strValue = i18n`forever`
+ } else {
+ strValue = formatDuration(intervalToDuration({ start: 0, end: value.d_ms
}), {
+ locale: {
+ formatDistance: (name, value) => {
+ switch(name) {
+ case 'xMonths': return i18n`${value}M`;
+ case 'xYears': return i18n`${value}Y`;
+ case 'xDays': return i18n`${value}d`;
+ case 'xHours': return i18n`${value}h`;
+ case 'xMinutes': return i18n`${value}min`;
+ case 'xSeconds': return i18n`${value}sec`;
+ }
+ },
+ localize: {
+ day: () => 's',
+ month: () => 'm',
+ ordinalNumber: () => 'th',
+ dayPeriod: () => 'p',
+ quarter: () => 'w',
+ era: () => 'e'
+ }
+ },
+ })
}
+
+ return <div class="field is-horizontal">
+ <div class="field-label is-normal">
+ <label class="label">
+ {label}
+ {tooltip && <span class="icon" data-tooltip={tooltip}>
+ <i class="mdi mdi-information" />
+ </span>}
+ </label>
+
+ </div>
+ <div class="field-body is-flex-grow-3">
+ <div class="field">
+ <div class="field has-addons">
+ <p class={expand ? "control is-expanded " : "control "}>
+ <input class="input" type="text"
+ readonly value={strValue}
+ placeholder={placeholder}
+ onClick={() => { if (!readonly) setOpened(true) }}
+ />
+ {required && <span class="icon has-text-danger is-right">
+ <i class="mdi mdi-alert" />
+ </span>}
+ {help}
+ </p>
+ <div class="control" onClick={() => { if (!readonly) setOpened(true)
}}>
+ <a class="button is-static" >
+ <span class="icon"><i class="mdi mdi-clock" /></span>
+ </a>
+ </div>
+ </div>
+ {error && <p class="help is-danger">{error}</p>}
+ </div>
+ {!readonly && <span data-tooltip={i18n`change value to empty`}>
+ <button class="button is-info mr-3" onClick={() => onChange(undefined
as any)} ><Translate>clear</Translate></button>
+ </span>}
+ {withForever && <span data-tooltip={i18n`change value to never`}>
+ <button class="button is-info" onClick={() => onChange({ d_ms:
'forever' } as any)}><Translate>forever</Translate></button>
+ </span>}
+ </div>
+ {opened && <SimpleModal onCancel={() => setOpened(false)}>
+ <DurationPicker days hours minutes
+ value={!value || value.d_ms === 'forever' ? 0 : value.d_ms}
+ onChange={(v) => { onChange({ d_ms: v } as any) }}
+ />
+ </SimpleModal>}
+ </div>
}
diff --git
a/packages/frontend/src/components/instance/DefaultInstanceFormFields.tsx
b/packages/frontend/src/components/instance/DefaultInstanceFormFields.tsx
index 2bfbeda..2d7f93f 100644
--- a/packages/frontend/src/components/instance/DefaultInstanceFormFields.tsx
+++ b/packages/frontend/src/components/instance/DefaultInstanceFormFields.tsx
@@ -79,6 +79,7 @@ export function DefaultInstanceFormFields({ readonlyId,
showId }: { readonlyId?:
<InputDuration<Entity> name="default_wire_transfer_delay"
label={i18n`Default wire transfer delay`}
+ withForever
tooltip={i18n`Maximum time an exchange is allowed to delay wiring funds
to the merchant, enabling it to aggregate smaller payments into larger wire
transfers and reducing wire fees.`} />
</Fragment>;
diff --git a/packages/frontend/src/components/modal/index.tsx
b/packages/frontend/src/components/modal/index.tsx
index 963dc05..a427215 100644
--- a/packages/frontend/src/components/modal/index.tsx
+++ b/packages/frontend/src/components/modal/index.tsx
@@ -82,6 +82,18 @@ export function ContinueModal({ active, description,
onCancel, onConfirm, childr
</div>
}
+export function SimpleModal({onCancel, children}: any):VNode {
+ return <div class="modal is-active">
+ <div class="modal-background " onClick={onCancel} />
+ <div class="modal-card">
+ <section class="modal-card-body is-main-section">
+ {children}
+ </section>
+ </div>
+ <button class="modal-close is-large " aria-label="close" onClick={onCancel}
/>
+</div>
+}
+
export function ClearConfirmModal({ description, onCancel, onClear, onConfirm,
children }: Props & { onClear?: () => void }): VNode {
return <div class="modal is-active">
<div class="modal-background " onClick={onCancel} />
diff --git a/packages/frontend/src/paths/instance/orders/create/CreatePage.tsx
b/packages/frontend/src/paths/instance/orders/create/CreatePage.tsx
index 87c9cc5..22fa2f3 100644
--- a/packages/frontend/src/paths/instance/orders/create/CreatePage.tsx
+++ b/packages/frontend/src/paths/instance/orders/create/CreatePage.tsx
@@ -121,27 +121,28 @@ function undefinedIfEmpty<T>(obj: T): T | undefined {
}
export function CreatePage({ onCreate, onBack, instanceConfig,
instanceInventory }: Props): VNode {
+ const [value, valueHandler] = useState(with_defaults(instanceConfig))
const config = useConfigContext()
const zero = Amounts.getZero(config.currency)
- const [value, valueHandler] = useState(with_defaults(instanceConfig))
-
- const inventoryList = Object.values(value.inventoryProducts || {})
- const productList = Object.values(value.products || {})
- const i18n = useTranslator()
+ const inventoryList = Object.values(value.inventoryProducts || {});
+ const productList = Object.values(value.products || {});
+ const i18n = useTranslator();
+
const errors: FormErrors<Entity> = {
pricing: undefinedIfEmpty({
summary: !value.pricing?.summary ? i18n`required` : undefined,
order_price: !value.pricing?.order_price ? i18n`required` : (
- (Amounts.parse(value.pricing.order_price)?.value || 0) <= 0 ?
i18n`must be greater than 0` : undefined
+ (Amounts.parse(value.pricing.order_price)?.value || 0) <= 0 ?
+ i18n`must be greater than 0` : undefined
)
}),
extra: value.extra && !stringIsValidJSON(value.extra) ? i18n`not a valid
json` : undefined,
payments: undefinedIfEmpty({
refund_deadline: !value.payments?.refund_deadline ? i18n`required` : (
!isFuture(value.payments.refund_deadline) ? i18n`should be in the
future` : (
- value.payments.pay_deadline && value.payments.refund_deadline &&
isBefore(value.payments.refund_deadline, value.payments.pay_deadline) ?
+ value.payments.pay_deadline &&
isBefore(value.payments.refund_deadline, value.payments.pay_deadline) ?
i18n`pay deadline cannot be before refund deadline` : undefined
)
),
@@ -151,8 +152,8 @@ export function CreatePage({ onCreate, onBack,
instanceConfig, instanceInventory
auto_refund_deadline: !value.payments?.auto_refund_deadline ? undefined
: (
!isFuture(value.payments.auto_refund_deadline) ? i18n`should be in the
future` : (
!value.payments?.refund_deadline ? i18n`should have a refund
deadline` : (
- !isAfter(value.payments.refund_deadline,
value.payments.auto_refund_deadline) ? i18n`auto refund cannot be after refund
deadline`
- : undefined
+ !isAfter(value.payments.refund_deadline,
value.payments.auto_refund_deadline) ?
+ i18n`auto refund cannot be after refund deadline` : undefined
)
)
),
diff --git a/packages/frontend/src/paths/instance/orders/create/index.tsx
b/packages/frontend/src/paths/instance/orders/create/index.tsx
index 71f5b7f..c447c4b 100644
--- a/packages/frontend/src/paths/instance/orders/create/index.tsx
+++ b/packages/frontend/src/paths/instance/orders/create/index.tsx
@@ -79,4 +79,4 @@ export default function OrderCreate({ onConfirm, onBack,
onLoadError, onNotFound
instanceInventory={inventoryResult.data}
/>
</Fragment>
-}
\ No newline at end of file
+}
diff --git a/packages/frontend/src/utils/constants.ts
b/packages/frontend/src/utils/constants.ts
index cbf4342..403adb9 100644
--- a/packages/frontend/src/utils/constants.ts
+++ b/packages/frontend/src/utils/constants.ts
@@ -23,7 +23,7 @@
export const PAYTO_REGEX =
/^payto:\/\/[a-zA-Z][a-zA-Z0-9-.]+(\/[a-zA-Z0-9\-\.\~\(\)@_%:!$&'*+,;=]*)*\??((amount|receiver-name|sender-name|instruction|message)=[a-zA-Z0-9\-\.\~\(\)@_%:!$'*+,;=]*&?)*$/
export const PAYTO_WIRE_METHOD_LOOKUP =
/payto:\/\/([a-zA-Z][a-zA-Z0-9-.]+)\/.*/
-export const AMOUNT_REGEX = /^[a-zA-Z]*:[0-9][0-9,]*\.?[0-9,]*$/
+export const AMOUNT_REGEX = /^[a-zA-Z][a-zA-Z]*:[0-9][0-9,]*\.?[0-9,]*$/
export const INSTANCE_ID_LOOKUP = /^\/instances\/([^/]*)\/?$/
--
To stop receiving notification emails like this one, please contact
gnunet@gnunet.org.
- [taler-merchant-backoffice] branch master updated (24491ab -> 61f845b), gnunet, 2021/06/24
- [taler-merchant-backoffice] 04/05: split payment and shipping, gnunet, 2021/06/24
- [taler-merchant-backoffice] 05/05: duration picker,
gnunet <=
- [taler-merchant-backoffice] 01/05: add qr, gnunet, 2021/06/24
- [taler-merchant-backoffice] 02/05: from star to alert, using amounts api, gnunet, 2021/06/24
- [taler-merchant-backoffice] 03/05: joint inventory product and custom products, gnunet, 2021/06/24