mirror of
https://github.com/joinmarket-webui/jam.git
synced 2026-08-16 13:01:05 +02:00
chore: add react-hook-form (#1000)
* build(deps): add react-hook-form * chore(rescan): use react-hook-form on rescan page * build(deps): update react-hook-form to v7.70.0 * chore(rescan): use yup for form schema validation
This commit is contained in:
parent
49fcba4dea
commit
fe4b1087f5
4 changed files with 182 additions and 59 deletions
80
package-lock.json
generated
80
package-lock.json
generated
|
|
@ -9,6 +9,7 @@
|
|||
"version": "0.1.0",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@hookform/resolvers": "5.2.2",
|
||||
"@joinmarket-webui/joinmarket-api-ts": "0.3.0",
|
||||
"@noble/hashes": "2.0.1",
|
||||
"@radix-ui/react-accordion": "1.2.12",
|
||||
|
|
@ -30,12 +31,14 @@
|
|||
"qrcode": "1.5.4",
|
||||
"react": "19.2.0",
|
||||
"react-dom": "19.2.0",
|
||||
"react-hook-form": "7.70.0",
|
||||
"react-i18next": "16.3.5",
|
||||
"react-router-dom": "7.9.6",
|
||||
"sonner": "2.0.7",
|
||||
"tailwind-merge": "3.4.0",
|
||||
"tailwindcss": "4.1.18",
|
||||
"tw-animate-css": "1.4.0",
|
||||
"yup": "1.7.1",
|
||||
"zustand": "5.0.9"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
|
@ -1041,6 +1044,18 @@
|
|||
"integrity": "sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@hookform/resolvers": {
|
||||
"version": "5.2.2",
|
||||
"resolved": "https://registry.npmjs.org/@hookform/resolvers/-/resolvers-5.2.2.tgz",
|
||||
"integrity": "sha512-A/IxlMLShx3KjV/HeTcTfaMxdwy690+L/ZADoeaTltLx+CVuzkeVIPuybK3jrRfw7YZnmdKsVVHAlEPIAEUNlA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@standard-schema/utils": "^0.3.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react-hook-form": "^7.55.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@humanfs/core": {
|
||||
"version": "0.19.1",
|
||||
"resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz",
|
||||
|
|
@ -2601,6 +2616,12 @@
|
|||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@standard-schema/utils": {
|
||||
"version": "0.3.0",
|
||||
"resolved": "https://registry.npmjs.org/@standard-schema/utils/-/utils-0.3.0.tgz",
|
||||
"integrity": "sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@storybook/addon-a11y": {
|
||||
"version": "10.1.11",
|
||||
"resolved": "https://registry.npmjs.org/@storybook/addon-a11y/-/addon-a11y-10.1.11.tgz",
|
||||
|
|
@ -7307,6 +7328,12 @@
|
|||
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/property-expr": {
|
||||
"version": "2.0.6",
|
||||
"resolved": "https://registry.npmjs.org/property-expr/-/property-expr-2.0.6.tgz",
|
||||
"integrity": "sha512-SVtmxhRE/CGkn3eZY1T6pC8Nln6Fr/lu1mKSgRud0eC73whjGfoAogbn78LkD8aFL0zz3bAFerKSnOl7NlErBA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/punycode": {
|
||||
"version": "2.3.1",
|
||||
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
|
||||
|
|
@ -7389,6 +7416,23 @@
|
|||
"react": "^19.2.0"
|
||||
}
|
||||
},
|
||||
"node_modules/react-hook-form": {
|
||||
"version": "7.70.0",
|
||||
"resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.70.0.tgz",
|
||||
"integrity": "sha512-COOMajS4FI3Wuwrs3GPpi/Jeef/5W1DRR84Yl5/ShlT3dKVFUfoGiEZ/QE6Uw8P4T2/CLJdcTVYKvWBMQTEpvw==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=18.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/react-hook-form"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^16.8.0 || ^17 || ^18 || ^19"
|
||||
}
|
||||
},
|
||||
"node_modules/react-i18next": {
|
||||
"version": "16.3.5",
|
||||
"resolved": "https://registry.npmjs.org/react-i18next/-/react-i18next-16.3.5.tgz",
|
||||
|
|
@ -8171,6 +8215,12 @@
|
|||
"url": "https://opencollective.com/webpack"
|
||||
}
|
||||
},
|
||||
"node_modules/tiny-case": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/tiny-case/-/tiny-case-1.0.3.tgz",
|
||||
"integrity": "sha512-Eet/eeMhkO6TX8mnUteS9zgPbUMQa4I6Kkp5ORiBD5476/m+PIRiumP5tmh5ioJpH7k51Kehawy2UDfsnxxY8Q==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/tiny-invariant": {
|
||||
"version": "1.3.3",
|
||||
"resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.3.tgz",
|
||||
|
|
@ -8271,6 +8321,12 @@
|
|||
"node": ">=8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/toposort": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/toposort/-/toposort-2.0.2.tgz",
|
||||
"integrity": "sha512-0a5EOkAUp8D4moMi2W8ZF8jcga7BgZd91O/yabJCFY8az+XSzeGyTKs0Aoo897iV1Nj6guFq8orWDS96z91oGg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/totalist": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/totalist/-/totalist-3.0.1.tgz",
|
||||
|
|
@ -8347,6 +8403,18 @@
|
|||
"node": ">= 0.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/type-fest": {
|
||||
"version": "2.19.0",
|
||||
"resolved": "https://registry.npmjs.org/type-fest/-/type-fest-2.19.0.tgz",
|
||||
"integrity": "sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA==",
|
||||
"license": "(MIT OR CC0-1.0)",
|
||||
"engines": {
|
||||
"node": ">=12.20"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/typescript": {
|
||||
"version": "5.9.3",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
|
||||
|
|
@ -9222,6 +9290,18 @@
|
|||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/yup": {
|
||||
"version": "1.7.1",
|
||||
"resolved": "https://registry.npmjs.org/yup/-/yup-1.7.1.tgz",
|
||||
"integrity": "sha512-GKHFX2nXul2/4Dtfxhozv701jLQHdf6J34YDh2cEkpqoo8le5Mg6/LrdseVLrFarmFygZTlfIhHx/QKfb/QWXw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"property-expr": "^2.0.5",
|
||||
"tiny-case": "^1.0.3",
|
||||
"toposort": "^2.0.2",
|
||||
"type-fest": "^2.19.0"
|
||||
}
|
||||
},
|
||||
"node_modules/zod": {
|
||||
"version": "4.1.12",
|
||||
"resolved": "https://registry.npmjs.org/zod/-/zod-4.1.12.tgz",
|
||||
|
|
|
|||
|
|
@ -44,6 +44,7 @@
|
|||
"*.{json,css,md}": "prettier --write"
|
||||
},
|
||||
"dependencies": {
|
||||
"@hookform/resolvers": "5.2.2",
|
||||
"@joinmarket-webui/joinmarket-api-ts": "0.3.0",
|
||||
"@noble/hashes": "2.0.1",
|
||||
"@radix-ui/react-accordion": "1.2.12",
|
||||
|
|
@ -65,12 +66,14 @@
|
|||
"qrcode": "1.5.4",
|
||||
"react": "19.2.0",
|
||||
"react-dom": "19.2.0",
|
||||
"react-hook-form": "7.70.0",
|
||||
"react-i18next": "16.3.5",
|
||||
"react-router-dom": "7.9.6",
|
||||
"sonner": "2.0.7",
|
||||
"tailwind-merge": "3.4.0",
|
||||
"tailwindcss": "4.1.18",
|
||||
"tw-animate-css": "1.4.0",
|
||||
"yup": "1.7.1",
|
||||
"zustand": "5.0.9"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
|
@ -88,6 +91,8 @@
|
|||
"@types/react-dom": "19.2.3",
|
||||
"@vitejs/plugin-react": "5.1.1",
|
||||
"@vitest/browser": "4.0.9",
|
||||
"@vitest/browser-playwright": "4.0.9",
|
||||
"@vitest/coverage-v8": "4.0.9",
|
||||
"conventional-changelog": "7.1.1",
|
||||
"eslint": "9.39.2",
|
||||
"eslint-plugin-react-hooks": "7.0.1",
|
||||
|
|
@ -102,9 +107,7 @@
|
|||
"typescript": "~5.9.3",
|
||||
"typescript-eslint": "8.51.0",
|
||||
"vite": "7.2.4",
|
||||
"vitest": "4.0.9",
|
||||
"@vitest/browser-playwright": "4.0.9",
|
||||
"@vitest/coverage-v8": "4.0.9"
|
||||
"vitest": "4.0.9"
|
||||
},
|
||||
"overrides": {
|
||||
"storybook": "$storybook",
|
||||
|
|
|
|||
|
|
@ -1,10 +1,13 @@
|
|||
import { useState } from 'react'
|
||||
import { yupResolver } from '@hookform/resolvers/yup'
|
||||
import { rescanblockchain } from '@joinmarket-webui/joinmarket-api-ts/jm'
|
||||
import { useMutation } from '@tanstack/react-query'
|
||||
import { ArrowLeft, RefreshCw } from 'lucide-react'
|
||||
import { useForm } from 'react-hook-form'
|
||||
import type { SubmitHandler } from 'react-hook-form'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
import { toast } from 'sonner'
|
||||
import * as yup from 'yup'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Card, CardContent } from '@/components/ui/card'
|
||||
import { Input } from '@/components/ui/input'
|
||||
|
|
@ -12,9 +15,87 @@ import { Label } from '@/components/ui/label'
|
|||
import { routes } from '@/constants/routes'
|
||||
import { useApiClient } from '@/hooks/useApiClient'
|
||||
import { useRescanStatus } from '@/hooks/useRescanStatus'
|
||||
import type { RescanInfo } from '@/hooks/useRescanStatus'
|
||||
import { SEGWIT_ACTIVATION_BLOCK } from '@/lib/utils'
|
||||
import type { WalletFileName } from '@/lib/utils'
|
||||
|
||||
type Inputs = {
|
||||
blockHeight: number
|
||||
}
|
||||
|
||||
const INPUT_BLOCK_HEIGHT_MIN = 0
|
||||
|
||||
const schema = yup
|
||||
.object({
|
||||
blockHeight: yup.number().integer().default(SEGWIT_ACTIVATION_BLOCK).min(INPUT_BLOCK_HEIGHT_MIN).required(),
|
||||
})
|
||||
.required()
|
||||
|
||||
interface RescanChainFormProps {
|
||||
rescanInfo: RescanInfo
|
||||
onSubmit: SubmitHandler<Inputs>
|
||||
disabled?: boolean
|
||||
}
|
||||
|
||||
function RescanChainForm({ rescanInfo, onSubmit, disabled }: RescanChainFormProps) {
|
||||
const { t } = useTranslation()
|
||||
const {
|
||||
register,
|
||||
handleSubmit,
|
||||
formState: { errors, isSubmitting, isValid },
|
||||
} = useForm<Inputs>({
|
||||
mode: 'all',
|
||||
defaultValues: {
|
||||
blockHeight: SEGWIT_ACTIVATION_BLOCK,
|
||||
},
|
||||
resolver: yupResolver(schema),
|
||||
})
|
||||
|
||||
return (
|
||||
/* "handleSubmit" will validate your inputs before invoking "onSubmit" */
|
||||
<form onSubmit={handleSubmit(onSubmit)} className="space-y-4">
|
||||
{/* include validation with required or other standard HTML validation rules */}
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="rescanHeight" className="text-sm font-medium">
|
||||
{t('rescan_chain.label_blockheight')}
|
||||
</Label>
|
||||
<p className="text-muted-foreground text-xs">{t('rescan_chain.description_blockheight')}</p>
|
||||
<div className="relative">
|
||||
<div className="absolute top-1/2 left-3 -translate-y-1/2">
|
||||
<RefreshCw className="text-muted-foreground h-4 w-4" />
|
||||
</div>
|
||||
|
||||
<Input
|
||||
{...register('blockHeight', {
|
||||
disabled: rescanInfo.rescanning,
|
||||
})}
|
||||
type="number"
|
||||
step={1}
|
||||
className="bg-background pl-10"
|
||||
/* TODO: i18n */
|
||||
placeholder="Enter block height"
|
||||
/>
|
||||
</div>
|
||||
{errors.blockHeight && (
|
||||
<div className="text-muted-foreground light:text-red-700 text-xs text-red-500">
|
||||
<span>{t('rescan_chain.feedback_invalid_blockheight', { min: INPUT_BLOCK_HEIGHT_MIN })}</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<Button
|
||||
type="submit"
|
||||
disabled={disabled || !isValid || isSubmitting || rescanInfo.rescanning}
|
||||
className="w-full"
|
||||
size="lg"
|
||||
>
|
||||
{isSubmitting || rescanInfo.rescanning
|
||||
? t('rescan_chain.text_button_submitting')
|
||||
: t('rescan_chain.text_button_submit')}
|
||||
</Button>
|
||||
</form>
|
||||
)
|
||||
}
|
||||
|
||||
interface RescanChainProps {
|
||||
walletFileName: WalletFileName
|
||||
}
|
||||
|
|
@ -24,7 +105,6 @@ export const RescanChain = ({ walletFileName }: RescanChainProps) => {
|
|||
const navigate = useNavigate()
|
||||
const client = useApiClient()
|
||||
const { rescanInfo, setRescanInfo } = useRescanStatus({ walletFileName })
|
||||
const [rescanHeight, setRescanHeight] = useState<number>(SEGWIT_ACTIVATION_BLOCK)
|
||||
|
||||
const rescanMutation = useMutation({
|
||||
mutationFn: async (blockHeight: number) => {
|
||||
|
|
@ -61,16 +141,19 @@ export const RescanChain = ({ walletFileName }: RescanChainProps) => {
|
|||
},
|
||||
})
|
||||
|
||||
const handleRescan = async () => {
|
||||
const blockHeight = rescanHeight
|
||||
if (isNaN(blockHeight) || blockHeight < 0) {
|
||||
toast.error(t('rescan_chain.feedback_invalid_blockheight', { min: 0 }))
|
||||
const handleRescan = async (blockHeight: number) => {
|
||||
if (isNaN(blockHeight) || blockHeight < INPUT_BLOCK_HEIGHT_MIN) {
|
||||
toast.error(t('rescan_chain.feedback_invalid_blockheight', { min: INPUT_BLOCK_HEIGHT_MIN }))
|
||||
return
|
||||
}
|
||||
|
||||
await rescanMutation.mutateAsync(blockHeight)
|
||||
}
|
||||
|
||||
const onSubmit: SubmitHandler<Inputs> = async (data) => {
|
||||
return handleRescan(data.blockHeight)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="mx-auto max-w-2xl space-y-3 p-4">
|
||||
<div className="flex items-center gap-3">
|
||||
|
|
@ -88,55 +171,12 @@ export const RescanChain = ({ walletFileName }: RescanChainProps) => {
|
|||
<p className="text-muted-foreground ml-11 text-sm">{t('rescan_chain.subtitle')}</p>
|
||||
|
||||
<Card className="ml-11 border-0 shadow-sm">
|
||||
<CardContent className="p-6">
|
||||
<div className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="rescanHeight" className="text-sm font-medium">
|
||||
{t('rescan_chain.label_blockheight')}
|
||||
</Label>
|
||||
<p className="text-muted-foreground text-xs">{t('rescan_chain.description_blockheight')}</p>
|
||||
<div className="relative">
|
||||
<div className="absolute top-1/2 left-3 -translate-y-1/2">
|
||||
<RefreshCw className="text-muted-foreground h-4 w-4" />
|
||||
</div>
|
||||
<Input
|
||||
id="rescanHeight"
|
||||
type="number"
|
||||
value={rescanHeight}
|
||||
onChange={(e) => setRescanHeight(parseInt(e.target.value))}
|
||||
className="bg-background pl-10"
|
||||
placeholder="Enter block height"
|
||||
disabled={rescanInfo.rescanning}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
onClick={handleRescan}
|
||||
disabled={!rescanHeight || rescanInfo.rescanning || rescanMutation.isPending}
|
||||
className="w-full"
|
||||
size="lg"
|
||||
>
|
||||
{rescanMutation.isPending || rescanInfo.rescanning
|
||||
? t('rescan_chain.text_button_submitting')
|
||||
: t('rescan_chain.text_button_submit')}
|
||||
</Button>
|
||||
|
||||
{rescanInfo.rescanning && (
|
||||
<div className="bg-muted/50 mt-4 rounded-lg p-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<RefreshCw className="h-4 w-4 animate-spin motion-reduce:hidden" />
|
||||
<span className="text-sm">
|
||||
{rescanInfo?.progress === undefined
|
||||
? t('app.alert_rescan_in_progress')
|
||||
: t('app.alert_rescan_in_progress_with_progress', {
|
||||
progress: rescanInfo.progress,
|
||||
})}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<CardContent className="px-6">
|
||||
<RescanChainForm
|
||||
rescanInfo={rescanInfo}
|
||||
onSubmit={onSubmit}
|
||||
disabled={rescanInfo.rescanning || rescanMutation.isPending}
|
||||
/>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ import { withQueryDelay } from '@/lib/queryClient'
|
|||
import { jmSessionStore } from '@/store/jmSessionStore'
|
||||
import { useApiClient } from './useApiClient'
|
||||
|
||||
interface RescanInfo {
|
||||
export interface RescanInfo {
|
||||
updatedAt: number
|
||||
rescanning: boolean
|
||||
progress?: number
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue