diff --git a/app/components/MergedStreamChart.test.tsx b/app/components/MergedStreamChart.test.tsx new file mode 100644 index 0000000..38403fc --- /dev/null +++ b/app/components/MergedStreamChart.test.tsx @@ -0,0 +1,43 @@ +import React from 'react'; +import { render, screen } from '@testing-library/react'; +import { MergedStreamChart } from './MergedStreamChart'; + +describe('MergedStreamChart', () => { + it('renders empty state when no streams are provided', () => { + render(); + expect(screen.getByText('No streams available')).toBeInTheDocument(); + }); + + it('renders aggregated progress correctly', () => { + const streams = [ + { id: '1', status: 'active' as const, accruedAmount: 10, totalAmount: 100, name: 'Stream 1' }, + { id: '2', status: 'active' as const, accruedAmount: 20, totalAmount: 100, name: 'Stream 2' }, + ]; + render(); + + expect(screen.getByText('Total Merged Progress')).toBeInTheDocument(); + expect(screen.getByText('Breakdown by Stream')).toBeInTheDocument(); + + // Total accrued = 30, Total amount = 200 -> 15% accrued + expect(screen.getByText('15% accrued')).toBeInTheDocument(); + + // Individual streams + expect(screen.getByText('Stream 1')).toBeInTheDocument(); + expect(screen.getByText('10% accrued')).toBeInTheDocument(); + + expect(screen.getByText('Stream 2')).toBeInTheDocument(); + expect(screen.getByText('20% accrued')).toBeInTheDocument(); + }); + + it('calculates aggregate status as ended when all streams are ended', () => { + const streams = [ + { id: '1', status: 'ended' as const, accruedAmount: 100, totalAmount: 100 }, + { id: '2', status: 'ended' as const, accruedAmount: 50, totalAmount: 50 }, + ]; + const { container } = render(); + // All ended streams are 100% completed + const completedLabels = screen.getAllByText('Completed'); + // 2 individual + 1 aggregated = 3 "Completed" labels + expect(completedLabels).toHaveLength(3); + }); +}); diff --git a/app/components/MergedStreamChart.tsx b/app/components/MergedStreamChart.tsx new file mode 100644 index 0000000..75dc804 --- /dev/null +++ b/app/components/MergedStreamChart.tsx @@ -0,0 +1,65 @@ +"use client"; + +import React from "react"; +import type { StreamStatus } from "@/app/types/openapi"; +import { StreamProgress } from "./StreamProgress"; + +export interface StreamData { + id: string; + status: StreamStatus; + accruedAmount?: number; + totalAmount?: number; + startedAt?: string; + endsAt?: string; + name?: string; +} + +export interface MergedStreamChartProps { + streams: StreamData[]; + className?: string; +} + +export function MergedStreamChart({ streams, className = "" }: MergedStreamChartProps) { + if (!streams || streams.length === 0) { + return
No streams available
; + } + + const totalAccrued = streams.reduce((sum, s) => sum + (s.accruedAmount || 0), 0); + const totalAmount = streams.reduce((sum, s) => sum + (s.totalAmount || 0), 0); + + const allEnded = streams.every(s => s.status === "ended" || s.status === "withdrawn" || s.status === "cancelled"); + const anyActive = streams.some(s => s.status === "active"); + const anyPaused = streams.some(s => s.status === "paused"); + + const aggregateStatus: StreamStatus = allEnded ? "ended" : (anyActive ? "active" : (anyPaused ? "paused" : "draft")); + + return ( +
+
+

Total Merged Progress

+ +
+
+

Breakdown by Stream

+
+ {streams.map(stream => ( +
+ {stream.name &&
{stream.name}
} + +
+ ))} +
+
+
+ ); +} diff --git a/docs/BODY-SIZE-LIMITS.md b/docs/BODY-SIZE-LIMITS.md index a08ec4e..334ba4f 100644 --- a/docs/BODY-SIZE-LIMITS.md +++ b/docs/BODY-SIZE-LIMITS.md @@ -2,6 +2,7 @@ ## Overview + Implemented per-route body size limit configuration for the GrantFox campaign, allowing different size limits for different API route categories: - **Default routes**: 256 KB (configurable via `MAX_STREAM_BODY_BYTES`)