Files
bincio-activity/mobile/app/activity/[id].tsx
T

165 lines
6.1 KiB
TypeScript

import { useLocalSearchParams, useRouter } from 'expo-router';
import { Pressable, ScrollView, StyleSheet, Text, View } from 'react-native';
import { useActivity } from '@/db/queries';
export default function ActivityScreen() {
const { id } = useLocalSearchParams<{ id: string }>();
const router = useRouter();
const row = useActivity(id);
if (!row) {
return (
<View style={styles.center}>
<Text style={styles.notFound}>Activity not found</Text>
</View>
);
}
const detail = JSON.parse(row.detail_json);
const km = detail.distance_m != null
? (detail.distance_m / 1000).toFixed(2)
: null;
const elev = detail.elevation_gain_m != null
? Math.round(detail.elevation_gain_m)
: null;
const elevLoss = detail.elevation_loss_m != null
? Math.round(Math.abs(detail.elevation_loss_m))
: null;
const movingTime = detail.moving_time_s != null
? formatDuration(detail.moving_time_s)
: null;
const speed = detail.avg_speed_kmh != null
? detail.avg_speed_kmh.toFixed(1)
: null;
const hr = detail.avg_hr_bpm != null
? Math.round(detail.avg_hr_bpm)
: null;
const power = detail.avg_power_w != null
? Math.round(detail.avg_power_w)
: null;
const date = new Date(detail.started_at).toLocaleDateString(undefined, {
weekday: 'long', day: 'numeric', month: 'long', year: 'numeric',
});
return (
<ScrollView style={styles.container} contentContainerStyle={styles.content}>
<Pressable style={styles.backButton} onPress={() => router.back()}>
<Text style={styles.backText}> Back</Text>
</Pressable>
<Text style={styles.sport}>{detail.sport ?? 'Activity'}</Text>
<Text style={styles.title}>{detail.title}</Text>
<Text style={styles.date}>{date}</Text>
{/* Map placeholder — Phase 1 */}
<View style={styles.mapPlaceholder}>
<Text style={styles.mapPlaceholderText}>Map · Phase 1</Text>
</View>
{/* Stats grid */}
<View style={styles.grid}>
{km && <StatCell label="Distance" value={km} unit="km" />}
{movingTime && <StatCell label="Moving time" value={movingTime} unit="" />}
{elev != null && <StatCell label="Elevation gain" value={String(elev)} unit="m" />}
{elevLoss != null && <StatCell label="Elevation loss" value={String(elevLoss)} unit="m" />}
{speed && <StatCell label="Avg speed" value={speed} unit="km/h" />}
{hr && <StatCell label="Avg HR" value={String(hr)} unit="bpm" />}
{power && <StatCell label="Avg power" value={String(power)} unit="W" />}
</View>
{/* Elevation chart placeholder — Phase 1 */}
{row.timeseries_json && (
<View style={styles.chartPlaceholder}>
<Text style={styles.mapPlaceholderText}>Elevation chart · Phase 1</Text>
</View>
)}
<View style={styles.meta}>
<MetaRow label="Source" value={detail.source ?? '—'} />
<MetaRow label="Device" value={detail.device ?? '—'} />
<MetaRow label="Origin" value={row.origin} />
<MetaRow label="Synced" value={row.synced_at ? new Date(row.synced_at * 1000).toLocaleDateString() : 'No'} />
</View>
</ScrollView>
);
}
function StatCell({ label, value, unit }: { label: string; value: string; unit: string }) {
return (
<View style={styles.statCell}>
<View style={styles.statValueRow}>
<Text style={styles.statValue}>{value}</Text>
{unit ? <Text style={styles.statUnit}>{unit}</Text> : null}
</View>
<Text style={styles.statLabel}>{label}</Text>
</View>
);
}
function MetaRow({ label, value }: { label: string; value: string }) {
return (
<View style={styles.metaRow}>
<Text style={styles.metaLabel}>{label}</Text>
<Text style={styles.metaValue}>{value}</Text>
</View>
);
}
function formatDuration(seconds: number): string {
const h = Math.floor(seconds / 3600);
const m = Math.floor((seconds % 3600) / 60);
const s = seconds % 60;
if (h > 0) return `${h}:${String(m).padStart(2, '0')}:${String(s).padStart(2, '0')}`;
return `${m}:${String(s).padStart(2, '0')}`;
}
const styles = StyleSheet.create({
container: { flex: 1, backgroundColor: '#09090b' },
content: { paddingBottom: 40 },
center: { flex: 1, alignItems: 'center', justifyContent: 'center', backgroundColor: '#09090b' },
notFound: { color: '#71717a', fontSize: 16 },
backButton: { paddingHorizontal: 16, paddingTop: 60, paddingBottom: 12 },
backText: { color: '#60a5fa', fontSize: 15 },
sport: { color: '#71717a', fontSize: 12, fontWeight: '600', letterSpacing: 0.8, paddingHorizontal: 16, marginBottom: 4 },
title: { color: '#f4f4f5', fontSize: 22, fontWeight: '700', paddingHorizontal: 16, marginBottom: 4 },
date: { color: '#71717a', fontSize: 13, paddingHorizontal: 16, marginBottom: 16 },
mapPlaceholder: {
height: 200, backgroundColor: '#18181b',
alignItems: 'center', justifyContent: 'center',
borderTopWidth: 1, borderBottomWidth: 1, borderColor: '#27272a',
marginBottom: 16,
},
chartPlaceholder: {
height: 120, backgroundColor: '#18181b',
alignItems: 'center', justifyContent: 'center',
borderTopWidth: 1, borderBottomWidth: 1, borderColor: '#27272a',
marginBottom: 16,
},
mapPlaceholderText: { color: '#3f3f46', fontSize: 13 },
grid: {
flexDirection: 'row', flexWrap: 'wrap',
paddingHorizontal: 12, gap: 8, marginBottom: 16,
},
statCell: {
backgroundColor: '#18181b', borderRadius: 10,
borderWidth: 1, borderColor: '#27272a',
padding: 14, width: '47%',
},
statValueRow: { flexDirection: 'row', alignItems: 'baseline', gap: 4, marginBottom: 4 },
statValue: { color: '#f4f4f5', fontSize: 24, fontWeight: '700' },
statUnit: { color: '#71717a', fontSize: 13 },
statLabel: { color: '#71717a', fontSize: 12 },
meta: {
marginHorizontal: 16, backgroundColor: '#18181b',
borderRadius: 10, borderWidth: 1, borderColor: '#27272a',
},
metaRow: {
flexDirection: 'row', justifyContent: 'space-between',
paddingHorizontal: 14, paddingVertical: 10,
borderBottomWidth: 1, borderBottomColor: '#27272a',
},
metaLabel: { color: '#71717a', fontSize: 13 },
metaValue: { color: '#a1a1aa', fontSize: 13 },
});