165 lines
6.1 KiB
TypeScript
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 },
|
|
});
|