UX improvements for mobile expense input
This commit is contained in:
@@ -2,6 +2,7 @@ import { useSignal } from '@preact/signals';
|
|||||||
import { useEffect } from 'preact/hooks';
|
import { useEffect } from 'preact/hooks';
|
||||||
|
|
||||||
import { Budget } from '/lib/types.ts';
|
import { Budget } from '/lib/types.ts';
|
||||||
|
import { formatInputToNumber } from '/lib/utils/misc.ts';
|
||||||
|
|
||||||
interface BudgetModalProps {
|
interface BudgetModalProps {
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
@@ -17,7 +18,7 @@ export default function BudgetModal(
|
|||||||
) {
|
) {
|
||||||
const newBudgetName = useSignal<string>(budget?.name ?? '');
|
const newBudgetName = useSignal<string>(budget?.name ?? '');
|
||||||
const newBudgetMonth = useSignal<string>(budget?.month ?? new Date().toISOString().substring(0, 10));
|
const newBudgetMonth = useSignal<string>(budget?.month ?? new Date().toISOString().substring(0, 10));
|
||||||
const newBudgetValue = useSignal<number>(budget?.value ?? 100);
|
const newBudgetValue = useSignal<number | string>(budget?.value ?? 100);
|
||||||
|
|
||||||
const resetForm = () => {
|
const resetForm = () => {
|
||||||
newBudgetName.value = '';
|
newBudgetName.value = '';
|
||||||
@@ -85,14 +86,13 @@ export default function BudgetModal(
|
|||||||
<label class='text-slate-300 block pb-1' for='budget_value'>Value</label>
|
<label class='text-slate-300 block pb-1' for='budget_value'>Value</label>
|
||||||
<input
|
<input
|
||||||
class='input-field'
|
class='input-field'
|
||||||
type='number'
|
type='text'
|
||||||
name='budget_value'
|
name='budget_value'
|
||||||
id='budget_value'
|
id='budget_value'
|
||||||
value={newBudgetValue.value}
|
value={newBudgetValue.value}
|
||||||
onInput={(event) => {
|
onInput={(event) => {
|
||||||
newBudgetValue.value = Number(event.currentTarget.value);
|
newBudgetValue.value = event.currentTarget.value;
|
||||||
}}
|
}}
|
||||||
min='0'
|
|
||||||
inputmode='decimal'
|
inputmode='decimal'
|
||||||
placeholder='100'
|
placeholder='100'
|
||||||
/>
|
/>
|
||||||
@@ -117,7 +117,12 @@ export default function BudgetModal(
|
|||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
class='px-5 py-2 bg-slate-700 hover:bg-slate-500 text-white cursor-pointer rounded-md ml-2'
|
class='px-5 py-2 bg-slate-700 hover:bg-slate-500 text-white cursor-pointer rounded-md ml-2'
|
||||||
onClick={() => onClickSave(newBudgetName.value, newBudgetMonth.value.substring(0, 7), newBudgetValue.value)}
|
onClick={() =>
|
||||||
|
onClickSave(
|
||||||
|
newBudgetName.value,
|
||||||
|
newBudgetMonth.value.substring(0, 7),
|
||||||
|
formatInputToNumber(newBudgetValue.value),
|
||||||
|
)}
|
||||||
>
|
>
|
||||||
{budget ? 'Update' : 'Create'}
|
{budget ? 'Update' : 'Create'}
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { useSignal } from '@preact/signals';
|
|||||||
import { useEffect } from 'preact/hooks';
|
import { useEffect } from 'preact/hooks';
|
||||||
|
|
||||||
import { Budget, Expense } from '/lib/types.ts';
|
import { Budget, Expense } from '/lib/types.ts';
|
||||||
|
import { formatInputToNumber } from '/lib/utils/misc.ts';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
RequestBody as SuggestionsRequestBody,
|
RequestBody as SuggestionsRequestBody,
|
||||||
@@ -27,7 +28,7 @@ interface ExpenseModalProps {
|
|||||||
export default function ExpenseModal(
|
export default function ExpenseModal(
|
||||||
{ isOpen, expense, budgets, onClickSave, onClickDelete, onClose, shouldResetForm }: ExpenseModalProps,
|
{ isOpen, expense, budgets, onClickSave, onClickDelete, onClose, shouldResetForm }: ExpenseModalProps,
|
||||||
) {
|
) {
|
||||||
const newExpenseCost = useSignal<number | ''>(expense?.cost ?? '');
|
const newExpenseCost = useSignal<number | string>(expense?.cost ?? '');
|
||||||
const newExpenseDescription = useSignal<string>(expense?.description ?? '');
|
const newExpenseDescription = useSignal<string>(expense?.description ?? '');
|
||||||
const newExpenseBudget = useSignal<string>(expense?.budget ?? 'Misc');
|
const newExpenseBudget = useSignal<string>(expense?.budget ?? 'Misc');
|
||||||
const newExpenseDate = useSignal<string>(expense?.date ?? '');
|
const newExpenseDate = useSignal<string>(expense?.date ?? '');
|
||||||
@@ -112,12 +113,12 @@ export default function ExpenseModal(
|
|||||||
<label class='text-slate-300 block pb-1' for='expense_cost'>Cost</label>
|
<label class='text-slate-300 block pb-1' for='expense_cost'>Cost</label>
|
||||||
<input
|
<input
|
||||||
class='input-field'
|
class='input-field'
|
||||||
type='number'
|
type='text'
|
||||||
name='expense_cost'
|
name='expense_cost'
|
||||||
id='expense_cost'
|
id='expense_cost'
|
||||||
value={newExpenseCost.value}
|
value={newExpenseCost.value}
|
||||||
onInput={(event) => {
|
onInput={(event) => {
|
||||||
newExpenseCost.value = Number(event.currentTarget.value);
|
newExpenseCost.value = event.currentTarget.value;
|
||||||
}}
|
}}
|
||||||
inputmode='decimal'
|
inputmode='decimal'
|
||||||
placeholder='10.99'
|
placeholder='10.99'
|
||||||
@@ -240,7 +241,7 @@ export default function ExpenseModal(
|
|||||||
class='px-5 py-2 bg-slate-700 hover:bg-slate-500 text-white cursor-pointer rounded-md ml-2'
|
class='px-5 py-2 bg-slate-700 hover:bg-slate-500 text-white cursor-pointer rounded-md ml-2'
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
onClickSave(
|
onClickSave(
|
||||||
newExpenseCost.value as number,
|
formatInputToNumber(newExpenseCost.value),
|
||||||
newExpenseDescription.value,
|
newExpenseDescription.value,
|
||||||
newExpenseBudget.value,
|
newExpenseBudget.value,
|
||||||
newExpenseDate.value,
|
newExpenseDate.value,
|
||||||
|
|||||||
@@ -268,3 +268,7 @@ export function formatNumber(currency: SupportedCurrencySymbol, number: number)
|
|||||||
maximumFractionDigits: 2,
|
maximumFractionDigits: 2,
|
||||||
}).format(Number.parseFloat(`${number}`.replace(',', '.')));
|
}).format(Number.parseFloat(`${number}`.replace(',', '.')));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function formatInputToNumber(numberInput: number | string): number {
|
||||||
|
return Number.parseFloat(`${numberInput}`.replace(',', '.'));
|
||||||
|
}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import {
|
|||||||
convertFormDataToObject,
|
convertFormDataToObject,
|
||||||
convertObjectToFormData,
|
convertObjectToFormData,
|
||||||
escapeHtml,
|
escapeHtml,
|
||||||
|
formatInputToNumber,
|
||||||
formatNumber,
|
formatNumber,
|
||||||
generateHash,
|
generateHash,
|
||||||
generateRandomCode,
|
generateRandomCode,
|
||||||
@@ -291,3 +292,23 @@ Deno.test('that formatNumber works', () => {
|
|||||||
assertEquals(result, test.expected);
|
assertEquals(result, test.expected);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
Deno.test('that formatInputToNumber works', () => {
|
||||||
|
const tests: { input: number | string; expected: number }[] = [
|
||||||
|
{ input: 42, expected: 42 },
|
||||||
|
{ input: '42', expected: 42 },
|
||||||
|
{ input: 42.5, expected: 42.5 },
|
||||||
|
{ input: '42.5', expected: 42.5 },
|
||||||
|
{ input: '42,5', expected: 42.5 },
|
||||||
|
{ input: '1234,56', expected: 1234.56 },
|
||||||
|
{ input: 0, expected: 0 },
|
||||||
|
{ input: '0', expected: 0 },
|
||||||
|
{ input: '0,0', expected: 0 },
|
||||||
|
{ input: '0.0', expected: 0 },
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const test of tests) {
|
||||||
|
const result = formatInputToNumber(test.input);
|
||||||
|
assertEquals(result, test.expected);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user