feat(components): TrackRow for history sessions

This commit is contained in:
fightrate-bot committed 2026-06-16 01:48:32 -03:00
1 parent 8232882977
commit edc10826aa
2 files changed
+221

No files matched your search

+56
View File
@@ -0,0 +1,56 @@
import React from 'react';
import TestRenderer from 'react-test-renderer';
import { TrackRow } from '../components/TrackRow';
const baseProps = {
trackNumber: 3,
day: '15/06',
hour: '19:30',
title: 'Sparring',
meta: '30 min · 600 amostras',
metrics: [
{ label: 'MÉD 152' },
{ label: 'PICO 184', tone: 'hot' as const },
],
isOpen: false,
onPress: jest.fn(),
accessibilityLabel: 'Janela 3',
};
describe('TrackRow', () => {
beforeEach(() => {
baseProps.onPress.mockClear();
});
it('renders track number zero-padded', () => {
let tree: any;
TestRenderer.act(() => {
tree = TestRenderer.create(React.createElement(TrackRow, { ...baseProps, testID: 'row' }));
});
const json = JSON.stringify(tree.toJSON());
expect(json).toContain('T03');
});
it('renders day, hour, title, and meta', () => {
let tree: any;
TestRenderer.act(() => {
tree = TestRenderer.create(React.createElement(TrackRow, { ...baseProps, testID: 'row' }));
});
const json = JSON.stringify(tree.toJSON());
expect(json).toContain('15/06');
expect(json).toContain('19:30');
expect(json).toContain('Sparring');
expect(json).toContain('30 min · 600 amostras');
});
it('renders metrics labels', () => {
let tree: any;
TestRenderer.act(() => {
tree = TestRenderer.create(React.createElement(TrackRow, { ...baseProps, testID: 'row' }));
});
const json = JSON.stringify(tree.toJSON());
expect(json).toContain('MÉD 152');
expect(json).toContain('PICO 184');
});
});
+165
View File
@@ -0,0 +1,165 @@
import React, { type ReactNode } from 'react';
import { Pressable, StyleSheet, Text, View, type ViewStyle } from 'react-native';
import { ChevronDown, ChevronUp } from 'lucide-react-native';
import { colors, spacing, typography } from '../theme';
type MetricTone = 'normal' | 'hot';
type Metric = {
readonly label: string;
readonly tone?: MetricTone;
};
type Props = {
readonly trackNumber: number;
readonly day: string;
readonly hour: string;
readonly title: string;
readonly meta: string;
readonly metrics: ReadonlyArray<Metric>;
readonly isOpen: boolean;
readonly onPress: () => void;
readonly accessibilityLabel: string;
readonly children?: ReactNode;
readonly testID?: string;
};
export function TrackRow({
trackNumber,
day,
hour,
title,
meta,
metrics,
isOpen,
onPress,
accessibilityLabel,
children,
testID,
}: Props) {
const trackNumberLabel = `T${String(trackNumber).padStart(2, '0')}`;
const sideRailStyle: ViewStyle = {
backgroundColor: isOpen ? colors.action : colors.line,
};
return (
<View>
<Pressable
accessibilityLabel={accessibilityLabel}
accessibilityRole="button"
onPress={onPress}
style={({ pressed }) => [styles.row, pressed ? styles.pressed : null]}
testID={testID}
>
<View style={[styles.sideRail, sideRailStyle]} />
<View style={styles.trackNumberWrap}>
<Text style={styles.trackNumber}>{trackNumberLabel}</Text>
</View>
<View style={styles.dateBlock}>
<Text style={styles.day}>{day}</Text>
<Text style={styles.hour}>{hour}</Text>
</View>
<View style={styles.body}>
<Text style={styles.title}>{title}</Text>
<Text style={styles.meta}>{meta}</Text>
<View style={styles.metricLine}>
{metrics.map((metric, index) => (
<Text
key={index}
style={[
styles.metricText,
metric.tone === 'hot' ? styles.metricTextHot : null,
]}
>
{metric.label}
</Text>
))}
</View>
</View>
<View style={styles.chevron}>
{isOpen ? (
<ChevronUp color={colors.warning} size={18} strokeWidth={2.6} />
) : (
<ChevronDown color={colors.textMuted} size={18} strokeWidth={2.6} />
)}
</View>
</Pressable>
{children}
</View>
);
}
const styles = StyleSheet.create({
row: {
flexDirection: 'row',
alignItems: 'center',
paddingVertical: spacing.sm,
paddingHorizontal: spacing.md,
gap: spacing.sm,
borderTopWidth: 1,
borderTopColor: colors.line,
},
pressed: {
opacity: 0.72,
},
sideRail: {
width: 2,
height: 40,
},
trackNumberWrap: {
width: 28,
alignItems: 'flex-start',
},
trackNumber: {
...typography.label,
color: colors.textMuted,
},
dateBlock: {
width: 56,
},
day: {
color: colors.warning,
fontSize: 18,
lineHeight: 21,
fontWeight: '900',
letterSpacing: -0.6,
},
hour: {
...typography.label,
color: colors.textDim,
marginTop: 2,
},
body: {
flex: 1,
},
title: {
color: colors.text,
fontSize: 18,
lineHeight: 22,
fontWeight: '900',
letterSpacing: -0.6,
},
meta: {
...typography.body,
color: colors.textMuted,
fontSize: 13,
lineHeight: 17,
marginTop: 2,
},
metricLine: {
flexDirection: 'row',
gap: spacing.sm,
marginTop: spacing.xs,
},
metricText: {
...typography.label,
color: colors.textMuted,
},
metricTextHot: {
color: colors.action,
},
chevron: {
width: 18,
alignItems: 'center',
justifyContent: 'center',
},
});