mirror of
https://github.com/priyanshujain/sanderling.git
synced 2026-10-03 11:37:09 +00:00
feat(folio-web): web sample app + CDP spec tests (#36)
* fix(runner): allow nil connection for web platform * fix(testrun): skip SDK handshake for web platform * feat(folio-web): add React/Vite web sample app * feat(folio-web): add sanderling spec * fix(chrome): use InsertText for multi-char text input * feat(hierarchy): add Screen field populated from sanderling-screen attr * fix(chrome): auto-detect viewport from CSS vars, fix InputText accumulation, expose route as screen * fix(runner): fall back to hierarchy root screen when snapshot screen is empty * fix(folio-web): broaden loggedIn extractor to all authenticated pages
This commit is contained in:
31 files changed
+2970
-74
No files matched your search
@@ -0,0 +1,194 @@
|
||||
import { useEffect, useState, type FormEvent } from 'react'
|
||||
import {
|
||||
createTransaction,
|
||||
getAccount,
|
||||
type Account,
|
||||
type TxnType,
|
||||
} from '../db'
|
||||
import { parseCents } from '../format'
|
||||
import { back, navigate } from '../router'
|
||||
import { BackButton, Header, Screen } from '../components/Screen'
|
||||
|
||||
export function AddTransactionPage(props: {
|
||||
accountId: string
|
||||
onCreated: () => void
|
||||
}) {
|
||||
const [account, setAccount] = useState<Account | null | undefined>(undefined)
|
||||
const [type, setType] = useState<TxnType>('credit')
|
||||
const [amount, setAmount] = useState('')
|
||||
const [note, setNote] = useState('')
|
||||
const [err, setErr] = useState<string | null>(null)
|
||||
const [busy, setBusy] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
let alive = true
|
||||
getAccount(props.accountId).then((a) => {
|
||||
if (alive) setAccount(a ?? null)
|
||||
})
|
||||
return () => {
|
||||
alive = false
|
||||
}
|
||||
}, [props.accountId])
|
||||
|
||||
async function onSubmit(e: FormEvent) {
|
||||
e.preventDefault()
|
||||
if (busy) return
|
||||
const cents = parseCents(amount)
|
||||
if (cents === null) {
|
||||
setErr('Enter a valid amount (e.g. 12.34)')
|
||||
return
|
||||
}
|
||||
if (cents <= 0) {
|
||||
setErr('Amount must be greater than zero')
|
||||
return
|
||||
}
|
||||
setBusy(true)
|
||||
try {
|
||||
await createTransaction({
|
||||
accountId: props.accountId,
|
||||
type,
|
||||
amount: cents,
|
||||
note,
|
||||
})
|
||||
props.onCreated()
|
||||
back(`/accounts/${props.accountId}`)
|
||||
} catch (e) {
|
||||
setErr(e instanceof Error ? e.message : 'Could not save transaction')
|
||||
} finally {
|
||||
setBusy(false)
|
||||
}
|
||||
}
|
||||
|
||||
if (account === null) {
|
||||
return (
|
||||
<Screen
|
||||
header={
|
||||
<Header
|
||||
title="Add transaction"
|
||||
left={<BackButton onClick={() => back('/')} />}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<div className="empty">
|
||||
<div className="empty-title">Account not found</div>
|
||||
<button
|
||||
className="btn"
|
||||
type="button"
|
||||
style={{ marginTop: 12, width: 'auto', padding: '10px 16px' }}
|
||||
onClick={() => navigate('/', { replace: true })}
|
||||
>
|
||||
Back to accounts
|
||||
</button>
|
||||
</div>
|
||||
</Screen>
|
||||
)
|
||||
}
|
||||
|
||||
const submitDisabled = busy || amount.trim() === ''
|
||||
|
||||
return (
|
||||
<Screen
|
||||
header={
|
||||
<Header
|
||||
title="Add transaction"
|
||||
subtitle={account?.name}
|
||||
left={
|
||||
<BackButton
|
||||
onClick={() => back(`/accounts/${props.accountId}`)}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
}
|
||||
footer={
|
||||
<button
|
||||
id="txn-submit"
|
||||
className="btn primary"
|
||||
type="submit"
|
||||
form="add-txn-form"
|
||||
disabled={submitDisabled}
|
||||
>
|
||||
{busy ? 'Saving…' : type === 'credit' ? 'Add credit' : 'Add debit'}
|
||||
</button>
|
||||
}
|
||||
>
|
||||
<form
|
||||
id="add-txn-form"
|
||||
onSubmit={onSubmit}
|
||||
className="list"
|
||||
style={{ gap: 16 }}
|
||||
data-account-id={props.accountId}
|
||||
data-type={type}
|
||||
noValidate
|
||||
>
|
||||
<div
|
||||
className="segmented"
|
||||
role="tablist"
|
||||
aria-label="Transaction type"
|
||||
>
|
||||
<button
|
||||
id="txn-credit"
|
||||
type="button"
|
||||
role="tab"
|
||||
className="credit"
|
||||
aria-pressed={type === 'credit'}
|
||||
onClick={() => setType('credit')}
|
||||
>
|
||||
Credit
|
||||
</button>
|
||||
<button
|
||||
id="txn-debit"
|
||||
type="button"
|
||||
role="tab"
|
||||
className="debit"
|
||||
aria-pressed={type === 'debit'}
|
||||
onClick={() => setType('debit')}
|
||||
>
|
||||
Debit
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="field">
|
||||
<label className="field-label" htmlFor="txn-amount">
|
||||
Amount
|
||||
</label>
|
||||
<input
|
||||
id="txn-amount"
|
||||
className={'input amount-input' + (err ? ' invalid' : '')}
|
||||
type="text"
|
||||
inputMode="decimal"
|
||||
autoComplete="off"
|
||||
placeholder="0.00"
|
||||
value={amount}
|
||||
onChange={(e) => {
|
||||
const v = e.target.value
|
||||
if (v === '' || /^\d*(\.\d{0,2})?$/.test(v)) {
|
||||
setAmount(v)
|
||||
if (err) setErr(null)
|
||||
}
|
||||
}}
|
||||
autoFocus
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="field">
|
||||
<label className="field-label" htmlFor="txn-note">
|
||||
Note (optional)
|
||||
</label>
|
||||
<input
|
||||
id="txn-note"
|
||||
className="input"
|
||||
type="text"
|
||||
placeholder="What's this for?"
|
||||
value={note}
|
||||
maxLength={80}
|
||||
onChange={(e) => setNote(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div id="txn-error" className="error" role="alert">
|
||||
{err ?? ''}
|
||||
</div>
|
||||
</form>
|
||||
</Screen>
|
||||
)
|
||||
}
|
||||
Reference in new issue
Block a user