Skip to content

Commit

Permalink
Merge branch 'adminV2' into SPV-960/FixAnyIssues
Browse files Browse the repository at this point in the history
  • Loading branch information
Nazarii-4chain authored Aug 29, 2024
2 parents 12dcda6 + 2209f32 commit 23de15e
Show file tree
Hide file tree
Showing 4 changed files with 167 additions and 75 deletions.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@
"sonner": "^1.4.41",
"tailwind-merge": "^2.3.0",
"tailwindcss-animate": "^1.0.7",
"use-debounce": "^10.0.1",
"use-debounce": "^10.0.3",
"zod": "^3.23.8"
},
"devDependencies": {
Expand Down
209 changes: 153 additions & 56 deletions src/components/AddXpubDialog/AddXpubDialog.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,3 @@
import { HD } from '@bsv/sdk';
import { useMutation, useQueryClient } from '@tanstack/react-query';
import { CirclePlus } from 'lucide-react';

import React, { useEffect, useState } from 'react';

import { toast } from 'sonner';

import { useDebounce } from 'use-debounce';

import { Button } from '@/components/ui';
import {
Dialog,
Expand All @@ -18,21 +8,57 @@ import {
DialogTitle,
DialogTrigger,
} from '@/components/ui/dialog.tsx';
import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from '@/components/ui/form';
import { Input } from '@/components/ui/input.tsx';
import { Label } from '@/components/ui/label.tsx';
import { useSpvWalletClient } from '@/contexts';
import { errorWrapper } from '@/utils';
import { HD } from '@bsv/sdk';
import { zodResolver } from '@hookform/resolvers/zod';
import { useMutation, useQueryClient } from '@tanstack/react-query';
import { CirclePlus, CircleX } from 'lucide-react';

import React, { useEffect, useRef, useState } from 'react';
import { useForm } from 'react-hook-form';

import { toast } from 'sonner';
import { useDebouncedCallback } from 'use-debounce';
import { z } from 'zod';

interface AddXpubDialogProps {
className?: string;
}

const formSchema = z.object({
xPriv: z.union([
z
.string()
.refine((val) => val.startsWith('xprv'), 'xPriv should start with xprv')
.refine((val) => val.length === 111, 'Invalid xPriv length.'),
z.literal(''),
]),
xPub: z
.string()
.refine((val) => val.startsWith('xpub'), 'xPub should starts with xpub.')
.refine((val) => val.length === 111, 'Invalid xPub length.'),
});

const xPrivSchema = formSchema.pick({ xPriv: true });

export const AddXpubDialog = ({ className }: AddXpubDialogProps) => {
const [xPriv, setXPriv] = useState<string>('');
const [xPub, setXPub] = useState<string>('');
const [debouncedXPriv] = useDebounce(xPriv, 500);
const [isOpen, setIsOpen] = useState(false);
const queryClient = useQueryClient();

const xPrivRef = useRef<HTMLInputElement>(null);
const xPubRef = useRef<HTMLInputElement>(null);

const form = useForm<z.infer<typeof formSchema>>({
resolver: zodResolver(formSchema),
defaultValues: {
xPriv: '',
xPub: '',
},
});

const { spvWalletClient } = useSpvWalletClient();

const mutation = useMutation({
Expand All @@ -43,73 +69,144 @@ export const AddXpubDialog = ({ className }: AddXpubDialogProps) => {
onSuccess: () => queryClient.invalidateQueries(),
});

const handleXPrivChange = (event: React.ChangeEvent<HTMLInputElement>) => {
setXPriv(event.target.value);
useEffect(() => {
form.reset();
}, [isOpen]);

const onSubmit = async (values: z.infer<typeof formSchema>) => {
const { xPub } = values;
try {
HD.fromString(values.xPub);
await mutation.mutateAsync(xPub);
toast.success('xPub successfully added');
form.reset();
} catch (error) {
toast.error('Unable to add xPub');
errorWrapper(error);
}
};

useEffect(() => {
setXPub('');
if (!xPriv) {
const debouncedXPriv = useDebouncedCallback(() => {
const parsedXPriv = xPrivSchema.safeParse({ xPriv: form.getValues('xPriv') });

if (form.getValues('xPriv').length === 0) {
form.clearErrors();
return;
}
form.trigger('xPriv');

if (!parsedXPriv.success) {
return;
}

try {
const xPrivHD = HD.fromString(debouncedXPriv);
const xPrivHD = HD.fromString(parsedXPriv.data.xPriv);
const xPubString = xPrivHD.toPublic().toString();
setXPub(xPubString);
form.setValue('xPub', xPubString);
toast.success('Converted xPriv to xPub');
} catch (error) {
toast.error('Unable to convert xPriv to xPub');
setXPub('');
}
}, [debouncedXPriv]);
}, 700);

const onSubmit = async () => {
if (!xPub) {
const debouncedXPub = useDebouncedCallback(() => {
if (form.getValues('xPub')?.length === 0) {
form.clearErrors();
return;
}
form.trigger('xPub');
}, 700);

try {
HD.fromString(xPub);
await mutation.mutateAsync(xPub);
toast.success('Added xPub');
setXPriv('');
setXPub('');
} catch (error) {
toast.error('Unable to add xPub');
errorWrapper(error);
const handeDialogToggle = () => {
setIsOpen((prev) => !prev);
form.reset();
};

const onXPrivChange = (e: React.ChangeEvent<HTMLInputElement>) => {
form.setValue('xPriv', e.target.value);
debouncedXPriv();
};

const onXPubChange = (e: React.ChangeEvent<HTMLInputElement>) => {
form.setValue('xPub', e.target.value);
form.resetField('xPriv');
debouncedXPub();
};

const onXPubClear = () => {
form.resetField('xPub');
if (xPubRef.current) {
xPubRef.current?.focus();
}
};

const onXPrivClear = () => {
form.resetField('xPriv');
if (xPrivRef.current) {
xPrivRef.current?.focus();
}
};

return (
<Dialog>
<Dialog open={isOpen} onOpenChange={handeDialogToggle}>
<DialogTrigger asChild className={className}>
<Button size="sm" variant="secondary" className="h-10 gap-1">
<CirclePlus className="mr-1" size={16} />
Add xPub
</Button>
</DialogTrigger>
<DialogContent className="sm:max-w-[425px]">
<DialogHeader>
<DialogTitle>Add xPub</DialogTitle>
<DialogDescription>Register a new xPub here.</DialogDescription>
</DialogHeader>
<div className="grid gap-4 py-4">
<div className="grid grid-cols-4 items-center gap-4">
<Label htmlFor="xPriv" className="text-right">
xPriv
</Label>
<Input id="xPriv" placeholder="xprv..." value={xPriv} onChange={handleXPrivChange} className="col-span-3" />
</div>
<div className="grid grid-cols-4 items-center gap-4">
<Label htmlFor="xPub" className="text-right">
xPub
</Label>
<Input id="xPub" readOnly value={xPub} className="col-span-3" />
</div>
</div>
<DialogFooter>
<Button onClick={onSubmit}>Add xPub</Button>
</DialogFooter>
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)}>
<DialogHeader>
<DialogTitle>Add xPub</DialogTitle>
<DialogDescription>Get xpub from xpriv</DialogDescription>
</DialogHeader>
<FormField
render={({ field }) => (
<FormItem className="relative">
<FormLabel>xPriv</FormLabel>
<FormControl>
<Input
placeholder="xprv..."
autoComplete="off"
className="pr-12"
{...field}
ref={xPrivRef}
onChange={onXPrivChange}
/>
</FormControl>
<CircleX size={16} className="absolute top-9 right-4 cursor-pointer" onClick={onXPrivClear} />
<FormMessage />
</FormItem>
)}
name="xPriv"
/>
<FormField
render={({ field }) => (
<FormItem className="mt-2 relative">
<FormLabel>xPub</FormLabel>
<FormControl>
<Input
autoComplete="off"
placeholder="xpub..."
className="pr-12"
{...field}
ref={xPubRef}
onChange={(e) => onXPubChange(e)}
/>
</FormControl>
<CircleX size={16} className="absolute top-9 right-4 cursor-pointer" onClick={onXPubClear} />
<FormMessage />
</FormItem>
)}
name="xPub"
/>
<DialogFooter className="mt-4">
<Button type="submit">Add xPub</Button>
</DialogFooter>
</form>
</Form>
</DialogContent>
</Dialog>
);
Expand Down
23 changes: 9 additions & 14 deletions src/routes/admin/_admin.xpub.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,11 @@
import { useSuspenseQuery } from '@tanstack/react-query';
import { createFileRoute, useSearch } from '@tanstack/react-router';
import { useState } from 'react';

import { useDebounce } from 'use-debounce';

import { z } from 'zod';

import {
AddXpubDialog,
CustomErrorComponent,
Expand All @@ -12,14 +20,7 @@ import {
} from '@/components';
import { useSpvWalletClient } from '@/contexts';

import { addStatusField, getDeletedElements, xPubQueryOptions } from '@/utils';
import { useSuspenseQuery } from '@tanstack/react-query';
import { createFileRoute, useSearch } from '@tanstack/react-router';
import { useState } from 'react';

import { useDebounce } from 'use-debounce';

import { z } from 'zod';
import { addStatusField, xPubQueryOptions } from '@/utils';

export const Route = createFileRoute('/admin/_admin/xpub')({
validateSearch: z.object({
Expand All @@ -45,8 +46,6 @@ export function Xpub() {

const mappedXpubs = addStatusField(xpubs);

const deletedXpubs = getDeletedElements(mappedXpubs);

// TODO: Add server pagination for xpubs when search and count will be merged

return (
Expand All @@ -55,7 +54,6 @@ export function Xpub() {
<div className="flex items-center justify-between">
<TabsList>
<TabsTrigger value="all">All</TabsTrigger>
<TabsTrigger value="deleted">Deleted</TabsTrigger>
</TabsList>
<div className="flex">
<AddXpubDialog className="mr-3" />
Expand All @@ -65,9 +63,6 @@ export function Xpub() {
<TabsContent value="all">
<XpubsTabContent xpubs={mappedXpubs} />
</TabsContent>
<TabsContent value="deleted">
<XpubsTabContent xpubs={deletedXpubs} />
</TabsContent>
</Tabs>
<Toaster position="bottom-center" />
</>
Expand Down
8 changes: 4 additions & 4 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -4948,10 +4948,10 @@ use-callback-ref@^1.3.0:
dependencies:
tslib "^2.0.0"

use-debounce@^10.0.1:
version "10.0.1"
resolved "https://registry.yarnpkg.com/use-debounce/-/use-debounce-10.0.1.tgz#adc42e7f7b08a237f5bbf0ea6cadee5813863cdc"
integrity sha512-0uUXjOfm44e6z4LZ/woZvkM8FwV1wiuoB6xnrrOmeAEjRDDzTLQNRFtYHvqUsJdrz1X37j0rVGIVp144GLHGKg==
use-debounce@^10.0.3:
version "10.0.3"
resolved "https://registry.yarnpkg.com/use-debounce/-/use-debounce-10.0.3.tgz#636094a37f7aa2bcc77b26b961481a0b571bf7ea"
integrity sha512-DxQSI9ZKso689WM1mjgGU3ozcxU1TJElBJ3X6S4SMzMNcm2lVH0AHmyXB+K7ewjz2BSUKJTDqTcwtSMRfB89dg==

use-sidecar@^1.1.2:
version "1.1.2"
Expand Down

0 comments on commit 23de15e

Please sign in to comment.