refactor(ts): convert tracker leaf components to TSX (B1)

First .tsx conversions — the tracker leaf components, where the branded Dollars
now flows into the UI: StatusBadge (status: TrackerStatus), FilterChip,
PaymentProgress (paymentSummary's Dollars → fmt type-checks), SummaryCards
(SummaryCard value: Dollars, CardDef/CardType/TrendInfo typed). Props interfaces
added; no behavior change. Confirms the .tsx pattern: a typed parent importing
untyped .jsx children is fine (children accept any props — no forced cascade).

typecheck 0, lint 0 errors, build green, 48 client tests, 17/17 e2e probe (home
page renders).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
null 2026-07-03 22:31:35 -05:00
parent 4af738f947
commit dc675fbecd
4 changed files with 61 additions and 9 deletions

View File

@ -1,6 +1,13 @@
import type { ReactNode } from 'react';
import { cn } from '@/lib/utils';
export function FilterChip({ active, children, onClick }) {
interface FilterChipProps {
active?: boolean;
children?: ReactNode;
onClick?: () => void;
}
export function FilterChip({ active, children, onClick }: FilterChipProps) {
return (
<button
type="button"

View File

@ -1,7 +1,18 @@
import { cn, fmt } from '@/lib/utils';
import { paymentSummary } from '@/lib/trackerUtils';
import type { Dollars } from '@/lib/money';
import type { TrackerRow } from '@/types';
export function PaymentProgress({ row, threshold, onOpen, onMarkFullAmount, compact = false, className }) {
interface PaymentProgressProps {
row: TrackerRow;
threshold: Dollars;
onOpen?: () => void;
onMarkFullAmount?: () => void;
compact?: boolean;
className?: string;
}
export function PaymentProgress({ row, threshold, onOpen, onMarkFullAmount, compact = false, className }: PaymentProgressProps) {
const summary = paymentSummary(row, threshold);
const barTone = summary.remaining === 0
? 'bg-emerald-500'

View File

@ -2,8 +2,16 @@ import React, { useMemo } from 'react';
import { Loader2, AlertCircle } from 'lucide-react';
import { cn } from '@/lib/utils';
import { STATUS_META, isPaidStatus } from '@/lib/trackerUtils';
import type { TrackerStatus } from '@/types';
export const StatusBadge = React.memo(function StatusBadge({ status, clickable, onClick, loading }) {
interface StatusBadgeProps {
status: TrackerStatus;
clickable?: boolean;
onClick?: () => void;
loading?: boolean;
}
export const StatusBadge = React.memo(function StatusBadge({ status, clickable, onClick, loading }: StatusBadgeProps) {
const meta = useMemo(() => STATUS_META[status] || STATUS_META.upcoming, [status]);
const isSkipped = status === 'skipped';

View File

@ -1,7 +1,20 @@
import { TrendingUp, AlertCircle, Clock, CheckCircle2, Settings2 } from 'lucide-react';
import { TrendingUp, AlertCircle, Clock, CheckCircle2, Settings2, type LucideIcon } from 'lucide-react';
import { cn, fmt } from '@/lib/utils';
import type { Dollars } from '@/lib/money';
const CARD_DEFS = {
type CardType = 'starting' | 'paid' | 'remaining' | 'overdue';
interface CardDef {
label: string;
icon: LucideIcon;
bar: string;
glow: string;
borderActive?: string;
valueClass: string;
activateWhen: (v: number) => boolean;
}
const CARD_DEFS: Record<CardType, CardDef> = {
starting: {
label: 'Starting',
icon: TrendingUp,
@ -38,12 +51,17 @@ const CARD_DEFS = {
},
};
export function TrendIndicator({ trend }) {
interface TrendInfo {
direction: string;
percent_change: number;
}
export function TrendIndicator({ trend }: { trend?: TrendInfo | null }) {
if (!trend) return null;
const { direction, percent_change } = trend;
let icon, color, text;
let icon: string, color: string, text: string;
switch (direction) {
case 'up':
icon = '↑';
@ -73,7 +91,15 @@ export function TrendIndicator({ trend }) {
);
}
export function SummaryCard({ type, value, onEdit, hint, label }) {
interface SummaryCardProps {
type: CardType;
value?: Dollars;
onEdit?: () => void;
hint?: string;
label?: string;
}
export function SummaryCard({ type, value, onEdit, hint, label }: SummaryCardProps) {
const def = CARD_DEFS[type];
const isActive = def.activateWhen(value || 0);
const Icon = def.icon;
@ -118,7 +144,7 @@ export function SummaryCard({ type, value, onEdit, hint, label }) {
);
}
export function TrendCard({ trend }) {
export function TrendCard({ trend }: { trend?: TrendInfo | null }) {
if (!trend) return null;
return (