1- import { useCallback , useEffect , useMemo , useState , type ComponentProps } from 'react'
1+ import { useCallback , useEffect , useMemo , useState } from 'react'
22import {
33 createColumnHelper ,
44 flexRender ,
@@ -18,16 +18,15 @@ import { useQueryYieldgenReport, type EarnReportEntry } from '@/components/earn/
1818import { Alert , AlertTitle } from '@/components/ui/alert'
1919import { Button } from '@/components/ui/button'
2020import { Card , CardContent } from '@/components/ui/card'
21- import { Dialog , DialogContent , DialogHeader , DialogTitle } from '@/components/ui/dialog'
2221import { Input } from '@/components/ui/input'
2322import { Balance } from '@/components/ui/jam/Balance'
2423import { SortIcon } from '@/components/ui/jam/SortIcon'
2524import { TablePagination } from '@/components/ui/jam/TablePagination'
2625import { Spinner } from '@/components/ui/spinner'
2726import { 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'
2928import { jamSettingsStore } from '@/store/jamSettingsStore'
30- import type { AmountSats , Milliseconds , WithRequiredProperty } from '@/types/global'
29+ import type { AmountSats , Milliseconds } from '@/types/global'
3130import { EarnReportChart } from './EarnReportChart'
3231
3332const sumEarned = ( entries : EarnReportEntry [ ] , since : Date ) : AmountSats => {
@@ -59,14 +58,14 @@ const columnHelper = createColumnHelper<EarnReportEntry>()
5958
6059type 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}
0 commit comments