Skip to content
This repository was archived by the owner on Jun 16, 2026. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
148 changes: 89 additions & 59 deletions apps/mobile/app/(tabs)/explore.tsx
Original file line number Diff line number Diff line change
@@ -1,39 +1,42 @@
import { View, Text, ScrollView, TouchableOpacity, TextInput, StatusBar } from "react-native";
import { View, Text, ScrollView, TouchableOpacity, TextInput, StatusBar, ActivityIndicator } from "react-native";
import { SafeAreaView } from 'react-native-safe-area-context';
import { Image } from 'expo-image';
import { Ionicons, MaterialCommunityIcons } from '@expo/vector-icons';
import { useState } from 'react';
import { useState, useMemo } from 'react';
import { useExercises } from '../hooks/useExercises';
import AddExerciseModal from '../components/AddExerciseModal';
import type { LocalExercise } from '../db/exercises';

const categories = ['All', 'Chest', 'Back', 'Legs', 'Arms', 'Shoulders'];

const recentSearches = [
{
name: 'Barbell Bench Press',
muscle: 'Chest',
equipment: 'Barbell',
image: 'https://images.unsplash.com/photo-1581009146145-b5ef050c2e1e?auto=format&fit=crop&w=150&h=150&q=80',
},
{
name: 'Incline Dumbbell Press',
muscle: 'Chest',
equipment: 'Dumbbell',
image: 'https://images.unsplash.com/photo-1541534741688-6078c6bfb5c5?auto=format&fit=crop&w=150&h=150&q=80',
},
];
const categoryMuscleKeywords: Record<string, string[]> = {
Chest: ['chest'],
Back: ['back', 'lats', 'traps', 'rhomboids'],
Legs: ['legs', 'quads', 'glutes', 'hamstrings', 'calves'],
Arms: ['bicep', 'tricep', 'forearm'],
Shoulders: ['delt', 'shoulder'],
};

const popular = [
{ name: 'Squat (Barbell)', muscle: 'Legs', equipment: 'Barbell' },
{ name: 'Pull Up', muscle: 'Back', equipment: 'Bodyweight' },
{
name: 'Deadlift (Barbell)',
muscle: 'Back / Legs',
equipment: 'Barbell',
image: 'https://images.unsplash.com/photo-1599058917212-d750089bc07e?auto=format&fit=crop&w=150&h=150&q=80',
},
];
function matchesCategory(ex: LocalExercise, category: string): boolean {
if (category === 'All') return true;
const keywords = categoryMuscleKeywords[category] ?? [];
const muscles = [...ex.primary_muscles, ...ex.secondary_muscles].map((m) => m.toLowerCase());
return keywords.some((kw) => muscles.some((m) => m.includes(kw)));
}

export default function ExploreScreen() {
const [activeCategory, setActiveCategory] = useState(0);
const [search, setSearch] = useState('');
const [showAdd, setShowAdd] = useState(false);
const { exercises, isLoading, createExercise } = useExercises();

const filtered = useMemo(() => {
const cat = categories[activeCategory];
return exercises.filter(
(ex) =>
matchesCategory(ex, cat) &&
ex.name.toLowerCase().includes(search.toLowerCase()),
);
}, [exercises, activeCategory, search]);

return (
<SafeAreaView style={{ flex: 1, backgroundColor: '#0a0a0a' }} edges={['top']}>
Expand All @@ -43,9 +46,17 @@ export default function ExploreScreen() {
<View style={{ paddingHorizontal: 24, paddingTop: 8, paddingBottom: 0 }}>
<View style={{ flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', marginBottom: 16 }}>
<Text style={{ color: '#fff', fontSize: 24, fontWeight: '700', letterSpacing: -0.3 }}>Library</Text>
<TouchableOpacity style={{ width: 40, height: 40, borderRadius: 20, backgroundColor: '#18181b', alignItems: 'center', justifyContent: 'center', borderWidth: 1, borderColor: '#27272a' }}>
<Ionicons name="options-outline" size={20} color="#a1a1aa" />
</TouchableOpacity>
<View style={{ flexDirection: 'row', gap: 8 }}>
<TouchableOpacity
onPress={() => setShowAdd(true)}
style={{ width: 40, height: 40, borderRadius: 20, backgroundColor: '#18181b', alignItems: 'center', justifyContent: 'center', borderWidth: 1, borderColor: '#27272a' }}
>
<Ionicons name="add" size={22} color="#f4f4f5" />
</TouchableOpacity>
<TouchableOpacity style={{ width: 40, height: 40, borderRadius: 20, backgroundColor: '#18181b', alignItems: 'center', justifyContent: 'center', borderWidth: 1, borderColor: '#27272a' }}>
<Ionicons name="options-outline" size={20} color="#a1a1aa" />
</TouchableOpacity>
</View>
</View>

{/* Search */}
Expand All @@ -55,6 +66,8 @@ export default function ExploreScreen() {
placeholder="Find an exercise..."
placeholderTextColor="#52525b"
style={{ flex: 1, color: '#fff', fontSize: 14 }}
value={search}
onChangeText={setSearch}
/>
</View>

Expand Down Expand Up @@ -91,51 +104,68 @@ export default function ExploreScreen() {
</View>

{/* List */}
<ScrollView
style={{ flex: 1 }}
showsVerticalScrollIndicator={false}
contentContainerStyle={{ paddingHorizontal: 24, paddingBottom: 24 }}
>
<Text style={{ color: '#71717a', fontSize: 11, fontWeight: '700', textTransform: 'uppercase', letterSpacing: 1, marginTop: 16, marginBottom: 12 }}>
Recently Searched
</Text>
{recentSearches.map((ex) => <ExerciseRow key={ex.name} {...ex} />)}
{isLoading && exercises.length === 0 ? (
<View style={{ flex: 1, alignItems: 'center', justifyContent: 'center' }}>
<ActivityIndicator color="#a1a1aa" />
</View>
) : (
<ScrollView
style={{ flex: 1 }}
showsVerticalScrollIndicator={false}
contentContainerStyle={{ paddingHorizontal: 24, paddingBottom: 24 }}
>
{filtered.length === 0 ? (
<Text style={{ color: '#52525b', fontSize: 14, marginTop: 32, textAlign: 'center' }}>
No exercises found
</Text>
) : (
<>
<Text style={{ color: '#71717a', fontSize: 11, fontWeight: '700', textTransform: 'uppercase', letterSpacing: 1, marginTop: 16, marginBottom: 12 }}>
{filtered.length} exercise{filtered.length !== 1 ? 's' : ''}
</Text>
{filtered.map((ex) => (
<ExerciseRow key={ex.local_id} exercise={ex} />
))}
</>
)}
</ScrollView>
)}

<Text style={{ color: '#71717a', fontSize: 11, fontWeight: '700', textTransform: 'uppercase', letterSpacing: 1, marginTop: 24, marginBottom: 12 }}>
Popular
</Text>
{popular.map((ex) => <ExerciseRow key={ex.name} {...ex} />)}
</ScrollView>
<AddExerciseModal
visible={showAdd}
onClose={() => setShowAdd(false)}
onSubmit={createExercise}
/>
</SafeAreaView>
);
}

function ExerciseRow({ name, muscle, equipment, image }: { name: string; muscle: string; equipment: string; image?: string }) {
function ExerciseRow({ exercise }: { exercise: LocalExercise }) {
const muscle = exercise.primary_muscles[0] ?? exercise.exercise_type ?? '—';
const isPending = exercise.sync_status === 'pending_create';

return (
<View style={{
flexDirection: 'row',
alignItems: 'center',
backgroundColor: 'rgba(24,24,27,0.4)',
borderWidth: 1,
borderColor: 'rgba(39,39,42,0.5)',
borderColor: isPending ? 'rgba(113,113,122,0.4)' : 'rgba(39,39,42,0.5)',
borderRadius: 16,
padding: 12,
marginBottom: 12,
}}>
{image ? (
<Image
source={{ uri: image }}
style={{ width: 64, height: 64, borderRadius: 12, borderWidth: 1, borderColor: '#27272a' }}
contentFit="cover"
/>
) : (
<View style={{ width: 64, height: 64, borderRadius: 12, backgroundColor: '#27272a', borderWidth: 1, borderColor: '#3f3f46', alignItems: 'center', justifyContent: 'center' }}>
<MaterialCommunityIcons name="dumbbell" size={24} color="#a1a1aa" />
</View>
)}
<View style={{ width: 64, height: 64, borderRadius: 12, backgroundColor: '#27272a', borderWidth: 1, borderColor: '#3f3f46', alignItems: 'center', justifyContent: 'center' }}>
<MaterialCommunityIcons name="dumbbell" size={24} color="#a1a1aa" />
</View>
<View style={{ flex: 1, marginLeft: 16 }}>
<Text style={{ color: '#f4f4f5', fontSize: 14, fontWeight: '700' }}>{name}</Text>
<Text style={{ color: '#52525b', fontSize: 12, marginTop: 4 }}>{muscle} • {equipment}</Text>
<Text style={{ color: '#f4f4f5', fontSize: 14, fontWeight: '700' }}>{exercise.name}</Text>
<Text style={{ color: '#52525b', fontSize: 12, marginTop: 4 }}>
{muscle} • {exercise.exercise_type || 'exercise'}
</Text>
{isPending && (
<Text style={{ color: '#71717a', fontSize: 10, marginTop: 2 }}>syncing...</Text>
)}
</View>
<TouchableOpacity style={{ width: 32, height: 32, borderRadius: 16, backgroundColor: '#27272a', alignItems: 'center', justifyContent: 'center' }}>
<Ionicons name="add" size={18} color="#d4d4d8" />
Expand Down
Loading
Loading