116 lines
3.9 KiB
TypeScript
116 lines
3.9 KiB
TypeScript
import { useRouter } from 'expo-router';
|
|
import { FlatList, Pressable, StyleSheet, Text, View } from 'react-native';
|
|
import { useActivities, type ActivitySummary } from '@/db/queries';
|
|
|
|
export default function FeedScreen() {
|
|
const activities = useActivities();
|
|
|
|
if (activities.length === 0) {
|
|
return (
|
|
<View style={styles.empty}>
|
|
<Text style={styles.emptyIcon}>🚴</Text>
|
|
<Text style={styles.emptyTitle}>No activities yet</Text>
|
|
<Text style={styles.emptyBody}>
|
|
Go to Import to add a FIT, GPX, or TCX file.
|
|
</Text>
|
|
</View>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<View style={styles.container}>
|
|
<Text style={styles.header}>Feed</Text>
|
|
<FlatList
|
|
data={activities}
|
|
keyExtractor={(a) => a.id}
|
|
renderItem={({ item }) => <ActivityCard activity={item} />}
|
|
contentContainerStyle={styles.list}
|
|
/>
|
|
</View>
|
|
);
|
|
}
|
|
|
|
function ActivityCard({ activity }: { activity: ActivitySummary }) {
|
|
const router = useRouter();
|
|
const km = activity.distance_m != null
|
|
? (activity.distance_m / 1000).toFixed(1)
|
|
: null;
|
|
const elev = activity.elevation_gain_m != null
|
|
? Math.round(activity.elevation_gain_m)
|
|
: null;
|
|
const date = new Date(activity.started_at).toLocaleDateString(undefined, {
|
|
day: 'numeric', month: 'short', year: 'numeric',
|
|
});
|
|
|
|
return (
|
|
<Pressable
|
|
style={styles.card}
|
|
onPress={() => router.push(`/activity/${activity.id}`)}
|
|
>
|
|
<View style={styles.cardTop}>
|
|
<Text style={styles.sportIcon}>{sportIcon(activity.sport)}</Text>
|
|
<View style={styles.cardMeta}>
|
|
<Text style={styles.cardDate}>{date}</Text>
|
|
{!activity.synced_at && activity.origin === 'local' && (
|
|
<Text style={styles.unsyncedBadge}>local</Text>
|
|
)}
|
|
</View>
|
|
</View>
|
|
<Text style={styles.cardTitle} numberOfLines={1}>{activity.title}</Text>
|
|
<View style={styles.cardStats}>
|
|
{km && <Stat label="km" value={km} />}
|
|
{elev != null && <Stat label="m↑" value={String(elev)} />}
|
|
</View>
|
|
</Pressable>
|
|
);
|
|
}
|
|
|
|
function Stat({ label, value }: { label: string; value: string }) {
|
|
return (
|
|
<View style={styles.stat}>
|
|
<Text style={styles.statValue}>{value}</Text>
|
|
<Text style={styles.statLabel}>{label}</Text>
|
|
</View>
|
|
);
|
|
}
|
|
|
|
function sportIcon(sport: string): string {
|
|
const icons: Record<string, string> = {
|
|
cycling: '🚴', running: '🏃', hiking: '🥾', swimming: '🏊', walking: '🚶',
|
|
};
|
|
return icons[sport] ?? '🏅';
|
|
}
|
|
|
|
const styles = StyleSheet.create({
|
|
container: { flex: 1, backgroundColor: '#09090b' },
|
|
header: {
|
|
color: '#fff', fontSize: 22, fontWeight: '700',
|
|
paddingHorizontal: 16, paddingTop: 60, paddingBottom: 12,
|
|
},
|
|
list: { padding: 16, gap: 12 },
|
|
card: {
|
|
backgroundColor: '#18181b', borderRadius: 12,
|
|
padding: 16, borderWidth: 1, borderColor: '#27272a',
|
|
},
|
|
cardTop: { flexDirection: 'row', justifyContent: 'space-between', marginBottom: 6 },
|
|
sportIcon: { fontSize: 20 },
|
|
cardMeta: { flexDirection: 'row', alignItems: 'center', gap: 8 },
|
|
cardDate: { color: '#71717a', fontSize: 12 },
|
|
unsyncedBadge: {
|
|
color: '#a1a1aa', fontSize: 10, borderWidth: 1,
|
|
borderColor: '#3f3f46', borderRadius: 4, paddingHorizontal: 4,
|
|
},
|
|
cardTitle: { color: '#f4f4f5', fontSize: 15, fontWeight: '600', marginBottom: 10 },
|
|
cardStats: { flexDirection: 'row', gap: 16 },
|
|
stat: { flexDirection: 'row', alignItems: 'baseline', gap: 3 },
|
|
statValue: { color: '#f4f4f5', fontSize: 16, fontWeight: '600' },
|
|
statLabel: { color: '#71717a', fontSize: 12 },
|
|
empty: {
|
|
flex: 1, backgroundColor: '#09090b',
|
|
alignItems: 'center', justifyContent: 'center', padding: 32,
|
|
},
|
|
emptyIcon: { fontSize: 48, marginBottom: 16 },
|
|
emptyTitle: { color: '#f4f4f5', fontSize: 18, fontWeight: '600', marginBottom: 8 },
|
|
emptyBody: { color: '#71717a', fontSize: 14, textAlign: 'center', lineHeight: 20 },
|
|
});
|