Skip to content

Commit 33ef27a

Browse files
chore(earn-report): Earn report as page and overlay
1 parent 8079959 commit 33ef27a

7 files changed

Lines changed: 215 additions & 150 deletions

File tree

src/App.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ import { queryClient } from '@/lib/queryClient'
4545
import { setIntervalDebounced, walletDisplayName, type WalletFileName } from '@/lib/utils'
4646
import { authStore } from '@/store/authStore'
4747
import { jamSettingsStore } from '@/store/jamSettingsStore'
48+
import { EarnReportPage } from './components/earn/report/EarnReportPage'
4849
import { LockWalletConfirmDialog } from './components/ui/jam/LockWalletConfirmDialog'
4950
import { Spinner } from './components/ui/spinner'
5051
import { WalletJarsDetailsPage } from './components/wallet/WalletJarsDetailsPage'
@@ -195,6 +196,7 @@ function App() {
195196
<Route path={routes.receive} element={<ReceivePage walletFileName={walletFileName!} />} />
196197
<Route path={routes.send} element={<SendPage walletFileName={walletFileName!} />} />
197198
<Route path={routes.earn} element={<EarnPage walletFileName={walletFileName!} />} />
199+
<Route path={routes.earnReport} element={<EarnReportPage walletFileName={walletFileName!} />} />
198200
<Route path={routes.sweep} element={<SweepPage walletFileName={walletFileName!} />} />
199201
<Route
200202
path={routes.settings}

src/components/earn/EarnPage.tsx

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ import { FidelityBondCard } from './FidelityBondCard'
3535
import { MoveToJarDialog } from './MoveToJarDialog'
3636
import { OfferCard } from './OfferCard'
3737
import { RenewBondDialog } from './RenewBondDialog'
38-
import { EarnReportDialog } from './report/EarnReportDialog'
38+
import { EarnReportOverlay } from './report/EarnReportOverlay'
3939

4040
// In order to prevent state mismatch, the 'maker stop' response is delayed shortly.
4141
// Even though the API response suggests that the maker has started or stopped immediately, it seems that this is not always the case.
@@ -375,8 +375,7 @@ export const EarnPage = ({ walletFileName }: EarnPageProps) => {
375375
</Card>
376376
)}
377377

378-
{/* Earn Report Sheet */}
379-
<EarnReportDialog open={showEarnReport} onOpenChange={setShowEarnReport} />
378+
<EarnReportOverlay open={showEarnReport} onOpenChange={setShowEarnReport} />
380379
</div>
381380
)
382381
}

src/components/earn/report/EarnReportDialog.tsx renamed to src/components/earn/report/EarnReportContent.tsx

Lines changed: 138 additions & 146 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { useCallback, useEffect, useMemo, useState, type ComponentProps } from 'react'
1+
import { useCallback, useEffect, useMemo, useState } from 'react'
22
import {
33
createColumnHelper,
44
flexRender,
@@ -18,16 +18,15 @@ import { useQueryYieldgenReport, type EarnReportEntry } from '@/components/earn/
1818
import { Alert, AlertTitle } from '@/components/ui/alert'
1919
import { Button } from '@/components/ui/button'
2020
import { Card, CardContent } from '@/components/ui/card'
21-
import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog'
2221
import { Input } from '@/components/ui/input'
2322
import { Balance } from '@/components/ui/jam/Balance'
2423
import { SortIcon } from '@/components/ui/jam/SortIcon'
2524
import { TablePagination } from '@/components/ui/jam/TablePagination'
2625
import { Spinner } from '@/components/ui/spinner'
2726
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table'
28-
import { BITCOIN_GENESIS_DATE, pseudoRandomFloat, pseudoRandomInteger } from '@/lib/utils'
27+
import { BITCOIN_GENESIS_DATE, cn, pseudoRandomFloat, pseudoRandomInteger } from '@/lib/utils'
2928
import { jamSettingsStore } from '@/store/jamSettingsStore'
30-
import type { AmountSats, Milliseconds, WithRequiredProperty } from '@/types/global'
29+
import type { AmountSats, Milliseconds } from '@/types/global'
3130
import { EarnReportChart } from './EarnReportChart'
3231

3332
const sumEarned = (entries: EarnReportEntry[], since: Date): AmountSats => {
@@ -59,14 +58,14 @@ const columnHelper = createColumnHelper<EarnReportEntry>()
5958

6059
type EarnReportColumnMeta = { align?: string; numeric?: boolean } | undefined
6160

62-
type EarnReportDialogProps = WithRequiredProperty<
63-
Omit<ComponentProps<typeof Dialog>, 'children'>,
64-
'open' | 'onOpenChange'
65-
>
61+
interface EarnReportContentProps {
62+
className?: string
63+
enabled: boolean
64+
}
6665

67-
export const EarnReportDialog = ({ open, onOpenChange, ...dialogProps }: EarnReportDialogProps) => {
66+
export const EarnReportContent = ({ className, enabled }: EarnReportContentProps) => {
6867
const { t } = useTranslation()
69-
const { data: entries, isLoading, refetch, isRefetching } = useQueryYieldgenReport({ enabled: open })
68+
const { data: entries, isLoading, refetch, isRefetching } = useQueryYieldgenReport({ enabled })
7069
const isDeveloperMode = useStore(jamSettingsStore, (state) => state.state.developerMode)
7170
const [now] = useState(() => Date.now())
7271

@@ -189,151 +188,144 @@ export const EarnReportDialog = ({ open, onOpenChange, ...dialogProps }: EarnRep
189188
const earned24Hours = useMemo(() => sumEarned(allEntries, new Date(now - MILLISECONDS_IN_A_DAY)), [allEntries, now])
190189

191190
return (
192-
<Dialog open={open} onOpenChange={onOpenChange} {...dialogProps}>
193-
<DialogContent className="data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom flex h-screen max-w-screen! flex-col rounded-none border-none">
194-
<DialogHeader>
195-
<DialogTitle>
196-
{t('earn.report.title')}
191+
<div className={cn('mx-auto flex-1 space-y-3', className)}>
192+
{isLoading ? (
193+
<div className="flex flex-1 items-center justify-center">
194+
<Spinner />
195+
</div>
196+
) : (
197+
<div className="flex min-h-0 flex-1 flex-col gap-4">
198+
{/* Stats row */}
199+
<div className="grid grid-cols-2 gap-2 lg:grid-cols-4">
200+
{(
201+
[
202+
{ label: t('earn.report.stats.earned_total'), value: earnedTotal },
203+
{ label: t('earn.report.stats.earned_90days'), value: earned90Days },
204+
{ label: t('earn.report.stats.earned_30days'), value: earned30Days },
205+
{ label: t('earn.report.stats.earned_24hours'), value: earned24Hours },
206+
] as const
207+
).map((stat) => (
208+
<Card key={stat.label} className="py-4">
209+
<CardContent className="text-center">
210+
<div className="text-muted-foreground text-xs">{stat.label}</div>
211+
<div className="mt-1 text-sm font-semibold sm:text-lg md:text-xl">
212+
<Balance valueString={String(stat.value)} />
213+
</div>
214+
</CardContent>
215+
</Card>
216+
))}
217+
</div>
218+
219+
{/* Daily earnings chart */}
220+
<EarnReportChart entries={allEntries} />
221+
222+
{/* Toolbar: search + refresh */}
223+
<div className="flex items-center gap-2">
224+
<div className="relative flex-1">
225+
<SearchIcon className="text-muted-foreground absolute top-1/2 left-2.5 size-4 -translate-y-1/2" />
226+
<Input
227+
placeholder={t('earn.report.placeholder_search')}
228+
value={globalFilter}
229+
onChange={(event) => setGlobalFilter(event.target.value)}
230+
className="pl-8"
231+
/>
232+
</div>
233+
<Button variant="outline" size="icon" onClick={() => void refetch()} disabled={isRefetching}>
234+
<RefreshCwIcon className={isRefetching ? 'animate-spin' : ''} />
235+
</Button>
236+
<Button variant="outline" size="sm" onClick={downloadCsv} disabled={allEntries.length === 0}>
237+
<DownloadIcon />
238+
{t('earn.report.text_button_download_csv')}
239+
</Button>
240+
197241
<span className="text-muted-foreground ml-2 text-sm font-normal">
198242
{globalFilter === ''
199243
? t('earn.report.text_report_summary', { count: allEntries.length })
200244
: t('earn.report.text_report_summary_filtered', { count: table.getFilteredRowModel().rows.length })}
201245
</span>
202-
</DialogTitle>
203-
</DialogHeader>
204-
205-
{isLoading ? (
206-
<div className="flex flex-1 items-center justify-center">
207-
<Spinner />
246+
{isDeveloperMode ? (
247+
<Button variant="outline" size="sm" onClick={addDemoEntry}>
248+
<PlusIcon />
249+
{t('earn.report.text_button_generate_demo_report')}
250+
<DevBadge />
251+
</Button>
252+
) : undefined}
208253
</div>
209-
) : (
210-
<div className="flex min-h-0 flex-1 flex-col gap-4 overflow-y-auto p-2 pt-0">
211-
{/* Stats row */}
212-
<div className="grid grid-cols-2 gap-2 lg:grid-cols-4">
213-
{(
214-
[
215-
{ label: t('earn.report.stats.earned_total'), value: earnedTotal },
216-
{ label: t('earn.report.stats.earned_90days'), value: earned90Days },
217-
{ label: t('earn.report.stats.earned_30days'), value: earned30Days },
218-
{ label: t('earn.report.stats.earned_24hours'), value: earned24Hours },
219-
] as const
220-
).map((stat) => (
221-
<Card key={stat.label} className="py-4">
222-
<CardContent className="text-center">
223-
<div className="text-muted-foreground text-xs">{stat.label}</div>
224-
<div className="mt-1 text-sm font-semibold sm:text-lg md:text-xl">
225-
<Balance valueString={String(stat.value)} />
226-
</div>
227-
</CardContent>
228-
</Card>
229-
))}
230-
</div>
231-
232-
{/* Daily earnings chart */}
233-
<EarnReportChart entries={allEntries} />
234254

235-
{/* Toolbar: search + refresh */}
236-
<div className="flex items-center gap-2">
237-
<div className="relative flex-1">
238-
<SearchIcon className="text-muted-foreground absolute top-1/2 left-2.5 size-4 -translate-y-1/2" />
239-
<Input
240-
placeholder={t('earn.report.placeholder_search')}
241-
value={globalFilter}
242-
onChange={(event) => setGlobalFilter(event.target.value)}
243-
className="pl-8"
244-
/>
245-
</div>
246-
<Button variant="outline" size="icon" onClick={() => void refetch()} disabled={isRefetching}>
247-
<RefreshCwIcon className={isRefetching ? 'animate-spin' : ''} />
248-
</Button>
249-
<Button variant="outline" size="sm" onClick={downloadCsv} disabled={allEntries.length === 0}>
250-
<DownloadIcon />
251-
{t('earn.report.text_button_download_csv')}
252-
</Button>
253-
{isDeveloperMode ? (
254-
<Button variant="outline" size="sm" onClick={addDemoEntry}>
255-
<PlusIcon />
256-
{t('earn.report.text_button_generate_demo_report')}
257-
<DevBadge />
258-
</Button>
259-
) : undefined}
255+
{/* Table or empty state */}
256+
{allEntries.length === 0 ? (
257+
<div className="space-y-2">
258+
<Alert variant="default">
259+
<AlertTitle>{t('earn.alert_empty_report')}</AlertTitle>
260+
</Alert>
260261
</div>
261-
262-
{/* Table or empty state */}
263-
{allEntries.length === 0 ? (
264-
<div className="space-y-2">
265-
<Alert variant="default">
266-
<AlertTitle>{t('earn.alert_empty_report')}</AlertTitle>
267-
</Alert>
268-
</div>
269-
) : (
270-
<div className="flex flex-1 flex-col gap-2 overflow-hidden rounded-lg border shadow-lg">
271-
<div className="flex-1 overflow-auto">
272-
<Table>
273-
<TableHeader>
274-
{table.getHeaderGroups().map((headerGroup) => (
275-
<TableRow key={headerGroup.id}>
276-
{headerGroup.headers.map((header) => {
277-
const canSort = header.column.getCanSort()
278-
const alignRight = (header.column.columnDef.meta as EarnReportColumnMeta)?.align === 'right'
279-
return (
280-
<TableHead
281-
key={header.id}
282-
className={canSort ? 'cursor-pointer select-none' : ''}
283-
onClick={canSort ? () => header.column.toggleSorting() : undefined}
262+
) : (
263+
<div className="flex flex-1 flex-col gap-2 overflow-hidden rounded-lg border shadow-lg">
264+
<div className="flex-1 overflow-auto">
265+
<Table>
266+
<TableHeader>
267+
{table.getHeaderGroups().map((headerGroup) => (
268+
<TableRow key={headerGroup.id}>
269+
{headerGroup.headers.map((header) => {
270+
const canSort = header.column.getCanSort()
271+
const alignRight = (header.column.columnDef.meta as EarnReportColumnMeta)?.align === 'right'
272+
return (
273+
<TableHead
274+
key={header.id}
275+
className={canSort ? 'cursor-pointer select-none' : ''}
276+
onClick={canSort ? () => header.column.toggleSorting() : undefined}
277+
>
278+
<div
279+
className={`flex items-center gap-2 ${alignRight ? 'justify-end' : ''} ${canSort ? 'cursor-pointer select-none' : ''} ${header.column.getIsSorted() ? 'font-bold' : ''} ${table.getState().sorting.length > 0 && !header.column.getIsSorted() ? 'text-muted-foreground' : ''}`}
284280
>
285-
<div
286-
className={`flex items-center gap-2 ${alignRight ? 'justify-end' : ''} ${canSort ? 'cursor-pointer select-none' : ''} ${header.column.getIsSorted() ? 'font-bold' : ''} ${table.getState().sorting.length > 0 && !header.column.getIsSorted() ? 'text-muted-foreground' : ''}`}
287-
>
288-
{flexRender(header.column.columnDef.header, header.getContext())}
289-
{canSort ? <SortIcon className="size-4" column={header.column} /> : undefined}
290-
</div>
291-
</TableHead>
292-
)
293-
})}
294-
</TableRow>
295-
))}
296-
</TableHeader>
297-
<TableBody>
298-
{visibleRows.map((row) => (
299-
<TableRow key={row.id}>
300-
{row.getVisibleCells().map((cell) => {
301-
const alignRight = (cell.column.columnDef.meta as EarnReportColumnMeta)?.align === 'right'
302-
return (
303-
<TableCell key={cell.id} className={alignRight ? 'text-right' : ''}>
304-
{flexRender(cell.column.columnDef.cell, cell.getContext())}
305-
</TableCell>
306-
)
307-
})}
308-
</TableRow>
309-
))}
310-
</TableBody>
311-
</Table>
312-
</div>
313-
314-
<TablePagination
315-
currentPage={currentPage}
316-
totalPages={totalPages}
317-
itemsPerPage={itemsPerPage}
318-
totalItems={allEntries.length}
319-
onPageChange={(page) => {
320-
setCurrentPage(page)
321-
table.setPageIndex(Math.max(0, page - 1))
322-
}}
323-
onItemsPerPageChange={(newItemsPerPage) => {
324-
setItemsPerPage(newItemsPerPage)
325-
const size =
326-
newItemsPerPage === -1 ? table.getPrePaginationRowModel().rows.length || 1 : newItemsPerPage
327-
table.setPageSize(size)
328-
setCurrentPage(1)
329-
table.setPageIndex(0)
330-
}}
331-
/>
281+
{flexRender(header.column.columnDef.header, header.getContext())}
282+
{canSort ? <SortIcon className="size-4" column={header.column} /> : undefined}
283+
</div>
284+
</TableHead>
285+
)
286+
})}
287+
</TableRow>
288+
))}
289+
</TableHeader>
290+
<TableBody>
291+
{visibleRows.map((row) => (
292+
<TableRow key={row.id}>
293+
{row.getVisibleCells().map((cell) => {
294+
const alignRight = (cell.column.columnDef.meta as EarnReportColumnMeta)?.align === 'right'
295+
return (
296+
<TableCell key={cell.id} className={alignRight ? 'text-right' : ''}>
297+
{flexRender(cell.column.columnDef.cell, cell.getContext())}
298+
</TableCell>
299+
)
300+
})}
301+
</TableRow>
302+
))}
303+
</TableBody>
304+
</Table>
332305
</div>
333-
)}
334-
</div>
335-
)}
336-
</DialogContent>
337-
</Dialog>
306+
307+
<TablePagination
308+
currentPage={currentPage}
309+
totalPages={totalPages}
310+
itemsPerPage={itemsPerPage}
311+
totalItems={allEntries.length}
312+
onPageChange={(page) => {
313+
setCurrentPage(page)
314+
table.setPageIndex(Math.max(0, page - 1))
315+
}}
316+
onItemsPerPageChange={(newItemsPerPage) => {
317+
setItemsPerPage(newItemsPerPage)
318+
const size =
319+
newItemsPerPage === -1 ? table.getPrePaginationRowModel().rows.length || 1 : newItemsPerPage
320+
table.setPageSize(size)
321+
setCurrentPage(1)
322+
table.setPageIndex(0)
323+
}}
324+
/>
325+
</div>
326+
)}
327+
</div>
328+
)}
329+
</div>
338330
)
339331
}
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import type { ComponentProps } from 'react'
2+
import { useTranslation } from 'react-i18next'
3+
import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog'
4+
import PageTitle from '@/components/ui/jam/PageTitle'
5+
import type { WithRequiredProperty } from '@/types/global'
6+
import { EarnReportContent } from './EarnReportContent'
7+
8+
type EarnReportOverlayProps = WithRequiredProperty<
9+
Omit<ComponentProps<typeof Dialog>, 'children'>,
10+
'open' | 'onOpenChange'
11+
>
12+
13+
export const EarnReportOverlay = ({ open, onOpenChange, ...dialogProps }: EarnReportOverlayProps) => {
14+
const { t } = useTranslation()
15+
16+
return (
17+
<Dialog open={open} onOpenChange={onOpenChange} {...dialogProps}>
18+
<DialogContent className="data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom flex h-screen max-w-screen! flex-col rounded-none border-none">
19+
<DialogHeader>
20+
<DialogTitle>
21+
<PageTitle title={t('earn.report.title')} />
22+
</DialogTitle>
23+
</DialogHeader>
24+
<div className="flex min-h-0 flex-1 overflow-hidden">
25+
<EarnReportContent enabled={open} className="flex h-full flex-col p-2 pt-0" />
26+
</div>
27+
</DialogContent>
28+
</Dialog>
29+
)
30+
}

0 commit comments

Comments
 (0)