Skip to content

Commit

Permalink
Merge pull request #22 from 0xdevcollins/feat/treasury-deposit-history
Browse files Browse the repository at this point in the history
Feat/treasury deposit history
  • Loading branch information
jonesjBSV authored Oct 29, 2024
2 parents 6d5fdc3 + ab5caa7 commit d10460e
Show file tree
Hide file tree
Showing 13 changed files with 1,106 additions and 0 deletions.
3 changes: 3 additions & 0 deletions app/(dashboard)/admin/page.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import AdminTreasuryHistory from '@/components/AdminTreasuryHistory';
import {
Card,
CardContent,
Expand All @@ -14,6 +15,8 @@ export default function AdminPage() {
<CardDescription>Admin items.</CardDescription>
</CardHeader>
<CardContent></CardContent>

<AdminTreasuryHistory />
</Card>
);
}
121 changes: 121 additions & 0 deletions app/(dashboard)/admin/wallet-settings/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
'use client'

import { useState } from 'react'
import { Form, useForm } from 'react-hook-form'
import { zodResolver } from '@hookform/resolvers/zod'
import * as z from 'zod'
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "@/components/ui/card"
import { Input } from "@/components/ui/input"
import { Button } from "@/components/ui/button"
import { FormField, FormItem, FormLabel, FormControl, FormDescription, FormMessage } from '@/components/ui/form'
import { useToast } from 'hooks/use-toast'

const formSchema = z.object({
threshold: z.number().min(0, "Threshold must be a positive number"),
emails: z.array(z.string().email("Invalid email address")).min(1, "At least one email is required"),
phoneNumbers: z.array(z.string().regex(/^\+?[1-9]\d{1,14}$/, "Invalid phone number")).optional(),
})

export default function WalletSettingsPage() {
const { toast } = useToast()
const [isLoading, setIsLoading] = useState(false)

const form = useForm<z.infer<typeof formSchema>>({
resolver: zodResolver(formSchema),
defaultValues: {
threshold: 0,
emails: [''],
phoneNumbers: [''],
},
})

async function onSubmit(values: z.infer<typeof formSchema>) {
setIsLoading(true)
try {
const response = await fetch('', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(values),
})
if (!response.ok) throw new Error('Failed to save settings')
toast({
title: "Settings saved",
description: "Your wallet alert settings have been updated successfully.",
})
} catch (error) {
toast({
title: "Error",
description: "Failed to save settings. Please try again.",
variant: "destructive",
})
} finally {
setIsLoading(false)
}
}

return (
<Card className="w-full max-w-2xl mx-auto">
<CardHeader>
<CardTitle>Wallet Alert Settings</CardTitle>
<CardDescription>Configure low-balance alerts for the faucet wallet</CardDescription>
</CardHeader>
<CardContent>
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-8">
<FormField
control={form.control}
name="threshold"
render={({ field }) => (
<FormItem>
<FormLabel>Low Balance Threshold (BSV)</FormLabel>
<FormControl>
<Input type="number" step="0.00000001" {...field} onChange={e => field.onChange(parseFloat(e.target.value))} />
</FormControl>
<FormDescription>
Set the wallet balance threshold that triggers an alert
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="emails"
render={({ field }) => (
<FormItem>
<FormLabel>Alert Email Addresses</FormLabel>
<FormControl>
<Input {...field} placeholder="Enter email addresses separated by commas" />
</FormControl>
<FormDescription>
Enter the email addresses that should receive alerts
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="phoneNumbers"
render={({ field }) => (
<FormItem>
<FormLabel>Alert Phone Numbers (Optional)</FormLabel>
<FormControl>
<Input {...field} placeholder="Enter phone numbers separated by commas" />
</FormControl>
<FormDescription>
Enter the phone numbers that should receive SMS alerts (if enabled)
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<Button type="submit" disabled={isLoading}>
{isLoading ? "Saving..." : "Save Settings"}
</Button>
</form>
</Form>
</CardContent>
</Card>
)
}
59 changes: 59 additions & 0 deletions app/api/deposit-history/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import { NextResponse } from 'next/server'

export type DepositTransaction = {
date: string
txid: string
beefTx: string
vout: number
txType: 'deposit'
amount: number
}

const mockDepositHistory: DepositTransaction[] = [
{
date: '2023-05-15T10:30:00',
txid: '1a2b3c4d5e6f7g8h9i0j',
beefTx: 'beef1234567890abcdef',
vout: 0,
txType: 'deposit',
amount: 1000000
},
{
date: '2023-05-14T14:45:00',
txid: '2b3c4d5e6f7g8h9i0j1a',
beefTx: 'beef0987654321fedcba',
vout: 1,
txType: 'deposit',
amount: 500000
},
{
date: '2023-05-13T09:15:00',
txid: '3c4d5e6f7g8h9i0j1a2b',
beefTx: 'beef2468135790acegik',
vout: 2,
txType: 'deposit',
amount: 750000
},
]

export async function GET() {
await new Promise(resolve => setTimeout(resolve, 1000))

// try {
// const response = await fetch('', {
// headers: {
// 'Authorization': `Bearer ${process.env.API_TOKEN}`,
// },
// })
// if (!response.ok) {
// throw new Error('Failed to fetch deposit history')
// }
// const data: DepositTransaction[] = await response.json()
// return NextResponse.json(data)
// } catch (error) {
// console.error('Error fetching deposit history:', error)
// return NextResponse.json({ error: 'Failed to fetch deposit history' }, { status: 500 })
// }

return NextResponse.json(mockDepositHistory)
}
101 changes: 101 additions & 0 deletions components/AdminTreasuryHistory.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
import { Suspense } from 'react'
import { format } from 'date-fns'
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/components/ui/card"
import { Skeleton } from '@/components/ui/skeleton'
import { DepositTransaction } from 'app/api/deposit-history/route'
import { Alert, AlertTitle, AlertDescription } from './ui/alert'
import { FileWarningIcon, RefreshCcwIcon } from 'lucide-react'

async function getDepositHistory(): Promise<DepositTransaction[]> {
const res = await fetch('http://localhost:3001/api/deposit-history', { cache: 'no-store' })
if (!res.ok) {
throw new Error('Failed to fetch deposit history')
}
return res.json()
}

function DepositHistoryTable({ depositHistory }: { depositHistory: DepositTransaction[] }) {
return (
<Table>
<TableHeader>
<TableRow>
<TableHead>Date</TableHead>
<TableHead>Txid</TableHead>
<TableHead>Beef TX</TableHead>
<TableHead>Vout</TableHead>
<TableHead>TX Type</TableHead>
<TableHead className="text-right">Amount (BSV)</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{depositHistory.map((transaction, index) => (
<TableRow key={index}>
<TableCell>{format(new Date(transaction.date), 'yyyy-MM-dd HH:mm:ss')}</TableCell>
<TableCell className="font-mono">{transaction.txid.slice(0, 10)}...</TableCell>
<TableCell className="font-mono">{transaction.beefTx.slice(0, 10)}...</TableCell>
<TableCell>{transaction.vout}</TableCell>
<TableCell>{transaction.txType}</TableCell>
<TableCell className="text-right">{(transaction.amount / 100000000).toFixed(8)}</TableCell>
</TableRow>
))}
</TableBody>
</Table>
)
}

function LoadingState() {
return (
<div className="space-y-3">
<Skeleton className="h-4 w-[250px]" />
<Skeleton className="h-4 w-[200px]" />
<Skeleton className="h-4 w-[150px]" />
<Skeleton className="h-4 w-[200px]" />
</div>
)
}

function ErrorState({ error }: { error: Error }) {
return (
<Alert variant="destructive">
<FileWarningIcon className="h-4 w-4" />
<AlertTitle>Error</AlertTitle>
<AlertDescription>
{error.message}
<button
onClick={() => window.location.reload()}
className="ml-2 inline-flex items-center px-2 py-1 border border-transparent text-xs font-medium rounded-md shadow-sm text-white bg-red-600 hover:bg-red-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-red-500"
>
<RefreshCcwIcon className="h-4 w-4 mr-1" /> Retry
</button>
</AlertDescription>
</Alert>
)
}

export default async function AdminTreasuryHistory() {
const depositHistoryPromise = getDepositHistory()

return (
<Card className="w-full">
<CardHeader>
<CardTitle className="text-2xl font-bold">Treasury - Deposit History</CardTitle>
<CardDescription>View all deposit transactions in the treasury</CardDescription>
</CardHeader>
<CardContent>
<Suspense fallback={<LoadingState />}>
<DepositHistoryContent promise={depositHistoryPromise} />
</Suspense>
</CardContent>
</Card>
)
}

async function DepositHistoryContent({ promise }: { promise: Promise<DepositTransaction[]> }) {
try {
const depositHistory = await promise
return <DepositHistoryTable depositHistory={depositHistory} />
} catch (error) {
return <ErrorState error={error instanceof Error ? error : new Error('Unknown error')} />
}
}
59 changes: 59 additions & 0 deletions components/ui/alert.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import * as React from "react"
import { cva, type VariantProps } from "class-variance-authority"

import { cn } from "@/lib/utils"

const alertVariants = cva(
"relative w-full rounded-lg border p-4 [&>svg~*]:pl-7 [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground",
{
variants: {
variant: {
default: "bg-background text-foreground",
destructive:
"border-destructive/50 text-destructive dark:border-destructive [&>svg]:text-destructive",
},
},
defaultVariants: {
variant: "default",
},
}
)

const Alert = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement> & VariantProps<typeof alertVariants>
>(({ className, variant, ...props }, ref) => (
<div
ref={ref}
role="alert"
className={cn(alertVariants({ variant }), className)}
{...props}
/>
))
Alert.displayName = "Alert"

const AlertTitle = React.forwardRef<
HTMLParagraphElement,
React.HTMLAttributes<HTMLHeadingElement>
>(({ className, ...props }, ref) => (
<h5
ref={ref}
className={cn("mb-1 font-medium leading-none tracking-tight", className)}
{...props}
/>
))
AlertTitle.displayName = "AlertTitle"

const AlertDescription = React.forwardRef<
HTMLParagraphElement,
React.HTMLAttributes<HTMLParagraphElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn("text-sm [&_p]:leading-relaxed", className)}
{...props}
/>
))
AlertDescription.displayName = "AlertDescription"

export { Alert, AlertTitle, AlertDescription }
Loading

0 comments on commit d10460e

Please sign in to comment.