summaryrefslogtreecommitdiff
path: root/packages/anastasis-webui/src/components
diff options
context:
space:
mode:
authorSebastian <sebasjm@gmail.com>2021-10-26 12:08:03 -0300
committerSebastian <sebasjm@gmail.com>2021-10-26 12:08:08 -0300
commit21b60c8f6ff69bf114779a767a3ac3355f69a34f (patch)
treec548dd7601b24d9dae4173d725656d35ce4bc26e /packages/anastasis-webui/src/components
parent835ac85a28dc11b5d7f5cff041ff7973d244e07e (diff)
downloadwallet-core-21b60c8f6ff69bf114779a767a3ac3355f69a34f.tar.gz
wallet-core-21b60c8f6ff69bf114779a767a3ac3355f69a34f.tar.bz2
wallet-core-21b60c8f6ff69bf114779a767a3ac3355f69a34f.zip
added core validators, worked on look and feel
Diffstat (limited to 'packages/anastasis-webui/src/components')
-rw-r--r--packages/anastasis-webui/src/components/fields/DateInput.tsx63
-rw-r--r--packages/anastasis-webui/src/components/fields/LabeledInput.tsx40
-rw-r--r--packages/anastasis-webui/src/components/menu/SideBar.tsx18
-rw-r--r--packages/anastasis-webui/src/components/picker/DatePicker.tsx324
-rw-r--r--packages/anastasis-webui/src/components/picker/DurationPicker.stories.tsx50
-rw-r--r--packages/anastasis-webui/src/components/picker/DurationPicker.tsx154
6 files changed, 638 insertions, 11 deletions
diff --git a/packages/anastasis-webui/src/components/fields/DateInput.tsx b/packages/anastasis-webui/src/components/fields/DateInput.tsx
new file mode 100644
index 000000000..c45acc6d2
--- /dev/null
+++ b/packages/anastasis-webui/src/components/fields/DateInput.tsx
@@ -0,0 +1,63 @@
+import { format } from "date-fns";
+import { h, VNode } from "preact";
+import { useLayoutEffect, useRef, useState } from "preact/hooks";
+import { DatePicker } from "../picker/DatePicker";
+
+export interface DateInputProps {
+ label: string;
+ grabFocus?: boolean;
+ tooltip?: string;
+ error?: string;
+ bind: [string, (x: string) => void];
+}
+
+export function DateInput(props: DateInputProps): VNode {
+ const inputRef = useRef<HTMLInputElement>(null);
+ useLayoutEffect(() => {
+ if (props.grabFocus) {
+ inputRef.current?.focus();
+ }
+ }, [props.grabFocus]);
+ const [opened, setOpened2] = useState(false)
+ function setOpened(v: boolean) {
+ console.log('dale', v)
+ setOpened2(v)
+ }
+
+ const value = props.bind[0];
+ const [dirty, setDirty] = useState(false)
+ const showError = dirty && props.error
+
+ return <div class="field">
+ <label class="label">
+ {props.label}
+ {props.tooltip && <span class="icon has-tooltip-right" data-tooltip={props.tooltip}>
+ <i class="mdi mdi-information" />
+ </span>}
+ </label>
+ <div class="control has-icons-right">
+ <input
+ type="text"
+ class={showError ? 'input is-danger' : 'input'}
+ onClick={() => { setOpened(true) }}
+ value={value}
+ ref={inputRef} />
+
+ <span class="control icon is-right">
+ <span class="icon"><i class="mdi mdi-calendar" /></span>
+ </span>
+ </div>
+ {showError && <p class="help is-danger">{props.error}</p>}
+ <DatePicker
+ opened={opened}
+ closeFunction={() => setOpened(false)}
+ dateReceiver={(d) => {
+ setDirty(true)
+ const v = format(d, 'yyyy/MM/dd')
+ props.bind[1](v);
+ }}
+ />
+ </div>
+ ;
+
+}
diff --git a/packages/anastasis-webui/src/components/fields/LabeledInput.tsx b/packages/anastasis-webui/src/components/fields/LabeledInput.tsx
new file mode 100644
index 000000000..96d634a4f
--- /dev/null
+++ b/packages/anastasis-webui/src/components/fields/LabeledInput.tsx
@@ -0,0 +1,40 @@
+import { h, VNode } from "preact";
+import { useLayoutEffect, useRef, useState } from "preact/hooks";
+
+export interface LabeledInputProps {
+ label: string;
+ grabFocus?: boolean;
+ error?: string;
+ tooltip?: string;
+ 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]);
+ const value = props.bind[0];
+ const [dirty, setDirty] = useState(false)
+ const showError = dirty && props.error
+ return (<div class="field">
+ <label class="label">
+ {props.label}
+ {props.tooltip && <span class="icon has-tooltip-right" data-tooltip={props.tooltip}>
+ <i class="mdi mdi-information" />
+ </span>}
+ </label>
+ <div class="control has-icons-right">
+ <input
+ value={value}
+ class={showError ? 'input is-danger' : 'input'}
+ onChange={(e) => {setDirty(true); props.bind[1]((e.target as HTMLInputElement).value)}}
+ ref={inputRef}
+ style={{ display: "block" }} />
+ </div>
+ {showError && <p class="help is-danger">{props.error}</p>}
+ </div>
+ );
+}
diff --git a/packages/anastasis-webui/src/components/menu/SideBar.tsx b/packages/anastasis-webui/src/components/menu/SideBar.tsx
index df582a5d0..12223d473 100644
--- a/packages/anastasis-webui/src/components/menu/SideBar.tsx
+++ b/packages/anastasis-webui/src/components/menu/SideBar.tsx
@@ -59,25 +59,21 @@ export function Sidebar({ mobile }: Props): VNode {
{!reducer.currentReducerState &&
<li>
<div class="ml-4">
- <span class="menu-item-label"><Translate>Start one options</Translate></span>
+ <span class="menu-item-label"><Translate>Select one option</Translate></span>
</div>
</li>
}
{reducer.currentReducerState && reducer.currentReducerState.backup_state ? <Fragment>
- <li class={reducer.currentReducerState.backup_state === BackupStates.ContinentSelecting ? 'is-active' : ''}>
+ <li class={
+ reducer.currentReducerState.backup_state === BackupStates.ContinentSelecting ||
+ reducer.currentReducerState.backup_state === BackupStates.CountrySelecting ? 'is-active' : ''}>
<div class="ml-4">
- <span class="menu-item-label"><Translate>Continent selection</Translate></span>
- </div>
- </li>
- <li class={reducer.currentReducerState.backup_state === BackupStates.CountrySelecting ? 'is-active' : ''}>
- <div class="ml-4">
- <span class="menu-item-label"><Translate>Country selection</Translate></span>
+ <span class="menu-item-label"><Translate>Location &amp; Currency</Translate></span>
</div>
</li>
<li class={reducer.currentReducerState.backup_state === BackupStates.UserAttributesCollecting ? 'is-active' : ''}>
<div class="ml-4">
-
- <span class="menu-item-label"><Translate>User attributes</Translate></span>
+ <span class="menu-item-label"><Translate>Personal information</Translate></span>
</div>
</li>
<li class={reducer.currentReducerState.backup_state === BackupStates.AuthenticationsEditing ? 'is-active' : ''}>
@@ -119,7 +115,7 @@ export function Sidebar({ mobile }: Props): VNode {
</Fragment> : (reducer.currentReducerState && reducer.currentReducerState?.recovery_state && <Fragment>
<li class={reducer.currentReducerState.recovery_state === RecoveryStates.ContinentSelecting ? 'is-active' : ''}>
<div class="ml-4">
- <span class="menu-item-label"><Translate>TruthsPaying</Translate></span>
+ <span class="menu-item-label"><Translate>ContinentSelecting</Translate></span>
</div>
</li>
<li class={reducer.currentReducerState.recovery_state === RecoveryStates.CountrySelecting ? 'is-active' : ''}>
diff --git a/packages/anastasis-webui/src/components/picker/DatePicker.tsx b/packages/anastasis-webui/src/components/picker/DatePicker.tsx
new file mode 100644
index 000000000..e51b3db68
--- /dev/null
+++ b/packages/anastasis-webui/src/components/picker/DatePicker.tsx
@@ -0,0 +1,324 @@
+/*
+ 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, Component } from "preact";
+
+interface Props {
+ closeFunction?: () => void;
+ dateReceiver?: (d: Date) => void;
+ opened?: boolean;
+}
+interface State {
+ displayedMonth: number;
+ displayedYear: number;
+ selectYearMode: boolean;
+ currentDate: Date;
+}
+const now = new Date()
+
+const monthArrShortFull = [
+ 'January',
+ 'February',
+ 'March',
+ 'April',
+ 'May',
+ 'June',
+ 'July',
+ 'August',
+ 'September',
+ 'October',
+ 'November',
+ 'December'
+]
+
+const monthArrShort = [
+ 'Jan',
+ 'Feb',
+ 'Mar',
+ 'Apr',
+ 'May',
+ 'Jun',
+ 'Jul',
+ 'Aug',
+ 'Sep',
+ 'Oct',
+ 'Nov',
+ 'Dec'
+]
+
+const dayArr = [
+ 'Sun',
+ 'Mon',
+ 'Tue',
+ 'Wed',
+ 'Thu',
+ 'Fri',
+ 'Sat'
+]
+
+const yearArr: number[] = []
+
+
+// inspired by https://codepen.io/m4r1vs/pen/MOOxyE
+export class DatePicker extends Component<Props, State> {
+
+ closeDatePicker() {
+ this.props.closeFunction && this.props.closeFunction(); // Function gets passed by parent
+ }
+
+ /**
+ * Gets fired when a day gets clicked.
+ * @param {object} e The event thrown by the <span /> element clicked
+ */
+ dayClicked(e: any) {
+
+ const element = e.target; // the actual element clicked
+
+ if (element.innerHTML === '') return false; // don't continue if <span /> empty
+
+ // get date from clicked element (gets attached when rendered)
+ const date = new Date(element.getAttribute('data-value'));
+
+ // update the state
+ this.setState({ currentDate: date });
+ this.passDateToParent(date)
+ }
+
+ /**
+ * returns days in month as array
+ * @param {number} month the month to display
+ * @param {number} year the year to display
+ */
+ getDaysByMonth(month: number, year: number) {
+
+ const calendar = [];
+
+ const date = new Date(year, month, 1); // month to display
+
+ const firstDay = new Date(year, month, 1).getDay(); // first weekday of month
+ const lastDate = new Date(year, month + 1, 0).getDate(); // last date of month
+
+ let day: number | null = 0;
+
+ // the calendar is 7*6 fields big, so 42 loops
+ for (let i = 0; i < 42; i++) {
+
+ if (i >= firstDay && day !== null) day = day + 1;
+ if (day !== null && day > lastDate) day = null;
+
+ // append the calendar Array
+ calendar.push({
+ day: (day === 0 || day === null) ? null : day, // null or number
+ date: (day === 0 || day === null) ? null : new Date(year, month, day), // null or Date()
+ today: (day === now.getDate() && month === now.getMonth() && year === now.getFullYear()) // boolean
+ });
+ }
+
+ return calendar;
+ }
+
+ /**
+ * Display previous month by updating state
+ */
+ displayPrevMonth() {
+ if (this.state.displayedMonth <= 0) {
+ this.setState({
+ displayedMonth: 11,
+ displayedYear: this.state.displayedYear - 1
+ });
+ }
+ else {
+ this.setState({
+ displayedMonth: this.state.displayedMonth - 1
+ });
+ }
+ }
+
+ /**
+ * Display next month by updating state
+ */
+ displayNextMonth() {
+ if (this.state.displayedMonth >= 11) {
+ this.setState({
+ displayedMonth: 0,
+ displayedYear: this.state.displayedYear + 1
+ });
+ }
+ else {
+ this.setState({
+ displayedMonth: this.state.displayedMonth + 1
+ });
+ }
+ }
+
+ /**
+ * Display the selected month (gets fired when clicking on the date string)
+ */
+ displaySelectedMonth() {
+ if (this.state.selectYearMode) {
+ this.toggleYearSelector();
+ }
+ else {
+ if (!this.state.currentDate) return false;
+ this.setState({
+ displayedMonth: this.state.currentDate.getMonth(),
+ displayedYear: this.state.currentDate.getFullYear()
+ });
+ }
+ }
+
+ toggleYearSelector() {
+ this.setState({ selectYearMode: !this.state.selectYearMode });
+ }
+
+ changeDisplayedYear(e: any) {
+ const element = e.target;
+ this.toggleYearSelector();
+ this.setState({ displayedYear: parseInt(element.innerHTML, 10), displayedMonth: 0 });
+ }
+
+ /**
+ * Pass the selected date to parent when 'OK' is clicked
+ */
+ passSavedDateDateToParent() {
+ this.passDateToParent(this.state.currentDate)
+ }
+ passDateToParent(date: Date) {
+ if (typeof this.props.dateReceiver === 'function') this.props.dateReceiver(date);
+ this.closeDatePicker();
+ }
+
+ componentDidUpdate() {
+ if (this.state.selectYearMode) {
+ document.getElementsByClassName('selected')[0].scrollIntoView(); // works in every browser incl. IE, replace with scrollIntoViewIfNeeded when browsers support it
+ }
+ }
+
+ constructor() {
+ super();
+
+ this.closeDatePicker = this.closeDatePicker.bind(this);
+ this.dayClicked = this.dayClicked.bind(this);
+ this.displayNextMonth = this.displayNextMonth.bind(this);
+ this.displayPrevMonth = this.displayPrevMonth.bind(this);
+ this.getDaysByMonth = this.getDaysByMonth.bind(this);
+ this.changeDisplayedYear = this.changeDisplayedYear.bind(this);
+ this.passDateToParent = this.passDateToParent.bind(this);
+ this.toggleYearSelector = this.toggleYearSelector.bind(this);
+ this.displaySelectedMonth = this.displaySelectedMonth.bind(this);
+
+
+ this.state = {
+ currentDate: now,
+ displayedMonth: now.getMonth(),
+ displayedYear: now.getFullYear(),
+ selectYearMode: false
+ }
+ }
+
+ render() {
+
+ const { currentDate, displayedMonth, displayedYear, selectYearMode } = this.state;
+
+ return (
+ <div>
+ <div class={`datePicker ${ this.props.opened && "datePicker--opened"}`}>
+
+ <div class="datePicker--titles">
+ <h3 style={{
+ color: selectYearMode ? 'rgba(255,255,255,.87)' : 'rgba(255,255,255,.57)'
+ }} onClick={this.toggleYearSelector}>{currentDate.getFullYear()}</h3>
+ <h2 style={{
+ color: !selectYearMode ? 'rgba(255,255,255,.87)' : 'rgba(255,255,255,.57)'
+ }} onClick={this.displaySelectedMonth}>
+ {dayArr[currentDate.getDay()]}, {monthArrShort[currentDate.getMonth()]} {currentDate.getDate()}
+ </h2>
+ </div>
+
+ {!selectYearMode && <nav>
+ <span onClick={this.displayPrevMonth} class="icon"><i style={{ transform: 'rotate(180deg)' }} class="mdi mdi-forward" /></span>
+ <h4>{monthArrShortFull[displayedMonth]} {displayedYear}</h4>
+ <span onClick={this.displayNextMonth} class="icon"><i class="mdi mdi-forward" /></span>
+ </nav>}
+
+ <div class="datePicker--scroll">
+
+ {!selectYearMode && <div class="datePicker--calendar" >
+
+ <div class="datePicker--dayNames">
+ {['S', 'M', 'T', 'W', 'T', 'F', 'S'].map((day,i) => <span key={i}>{day}</span>)}
+ </div>
+
+ <div onClick={this.dayClicked} class="datePicker--days">
+
+ {/*
+ Loop through the calendar object returned by getDaysByMonth().
+ */}
+
+ {this.getDaysByMonth(this.state.displayedMonth, this.state.displayedYear)
+ .map(
+ day => {
+ let selected = false;
+
+ if (currentDate && day.date) selected = (currentDate.toLocaleDateString() === day.date.toLocaleDateString());
+
+ return (<span key={day.day}
+ class={(day.today ? 'datePicker--today ' : '') + (selected ? 'datePicker--selected' : '')}
+ disabled={!day.date}
+ data-value={day.date}
+ >
+ {day.day}
+ </span>)
+ }
+ )
+ }
+
+ </div>
+
+ </div>}
+
+ {selectYearMode && <div class="datePicker--selectYear">
+
+ {yearArr.map(year => (
+ <span key={year} class={(year === displayedYear) ? 'selected' : ''} onClick={this.changeDisplayedYear}>
+ {year}
+ </span>
+ ))}
+
+ </div>}
+
+ </div>
+ </div>
+
+ <div class="datePicker--background" onClick={this.closeDatePicker} style={{
+ display: this.props.opened ? 'block' : 'none',
+ }}
+ />
+
+ </div>
+ )
+ }
+}
+
+
+for (let i = 2010; i <= now.getFullYear() + 10; i++) {
+ yearArr.push(i);
+}
diff --git a/packages/anastasis-webui/src/components/picker/DurationPicker.stories.tsx b/packages/anastasis-webui/src/components/picker/DurationPicker.stories.tsx
new file mode 100644
index 000000000..275c80fa6
--- /dev/null
+++ b/packages/anastasis-webui/src/components/picker/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/anastasis-webui/src/components/picker/DurationPicker.tsx b/packages/anastasis-webui/src/components/picker/DurationPicker.tsx
new file mode 100644
index 000000000..235a63e2d
--- /dev/null
+++ b/packages/anastasis-webui/src/components/picker/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 "../../scss/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 ss = 1000
+ const ms = ss * 60
+ const hs = ms * 60
+ const ds = hs * 24
+ const i18n = useTranslator()
+
+ return <div class="rdp-picker">
+ {days && <DurationColumn unit={i18n`days`} max={99}
+ value={Math.floor(value / ds)}
+ onDecrease={value >= ds ? () => onChange(value - ds) : undefined}
+ onIncrease={value < 99 * ds ? () => onChange(value + ds) : undefined}
+ onChange={diff => onChange(value + diff * ds)}
+ />}
+ {hours && <DurationColumn unit={i18n`hours`} max={23} min={1}
+ value={Math.floor(value / hs) % 24}
+ onDecrease={value >= hs ? () => onChange(value - hs) : undefined}
+ onIncrease={value < 99 * ds ? () => onChange(value + hs) : undefined}
+ onChange={diff => onChange(value + diff * hs)}
+ />}
+ {minutes && <DurationColumn unit={i18n`minutes`} max={59} min={1}
+ value={Math.floor(value / ms) % 60}
+ onDecrease={value >= ms ? () => onChange(value - ms) : undefined}
+ onIncrease={value < 99 * ds ? () => onChange(value + ms) : undefined}
+ onChange={diff => onChange(value + diff * ms)}
+ />}
+ {seconds && <DurationColumn unit={i18n`seconds`} max={59}
+ value={Math.floor(value / ss) % 60}
+ onDecrease={value >= ss ? () => onChange(value - ss) : undefined}
+ onIncrease={value < 99 * ds ? () => onChange(value + ss) : undefined}
+ onChange={diff => onChange(value + diff * ss)}
+ />}
+ </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 - 2}>
+ {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 + 2}>
+ {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