updated header in tabs and searchbar in library

This commit is contained in:
DocNR 2025-02-27 20:24:04 -05:00
parent 0d460e8f3e
commit 15c973f333
16 changed files with 537 additions and 374 deletions

View File

@ -1,28 +1,31 @@
// app/(tabs)/history.tsx
// app/(tabs)/social.tsx
import { View } from 'react-native';
import { Text } from '@/components/ui/text';
import { TabScreen } from '@/components/layout/TabScreen';
import Header from '@/components/Header';
import { Button } from '@/components/ui/button';
import { Filter } from 'lucide-react-native';
import { Bell } from 'lucide-react-native';
import Header from '@/components/Header';
import { TabScreen } from '@/components/layout/TabScreen';
export default function HistoryScreen() {
export default function SocialScreen() {
return (
<TabScreen>
<Header
title="History"
useLogo={true}
rightElement={
<Button
variant="ghost"
size="icon"
onPress={() => console.log('Open filters')}
onPress={() => console.log('Open notifications')}
>
<Filter className="text-foreground" />
<View className="relative">
<Bell className="text-foreground" />
<View className="absolute -top-1 -right-1 w-2 h-2 bg-primary rounded-full" />
</View>
</Button>
}
/>
<View className="flex-1 items-center justify-center">
<Text>History Screen</Text>
<Text>Social Screen</Text>
</View>
</TabScreen>
);

View File

@ -20,6 +20,8 @@ import FavoriteTemplate from '@/components/workout/FavoriteTemplate'
import { useWorkoutStore } from '@/stores/workoutStore'
import { Text } from '@/components/ui/text'
import { getRandomWorkoutTitle } from '@/utils/workoutTitles'
import { Bell } from 'lucide-react-native';
import { Button } from '@/components/ui/button';
interface FavoriteTemplateData {
id: string;
@ -204,7 +206,21 @@ export default function WorkoutScreen() {
return (
<TabScreen>
<Header title="Workout" />
<Header
useLogo={true}
rightElement={
<Button
variant="ghost"
size="icon"
onPress={() => console.log('Open notifications')}
>
<View className="relative">
<Bell className="text-foreground" />
<View className="absolute -top-1 -right-1 w-2 h-2 bg-primary rounded-full" />
</View>
</Button>
}
/>
<ScrollView
className="flex-1 px-4 pt-4"

View File

@ -1,77 +1,22 @@
// app/(tabs)/library/_layout.tsx
import { View } from 'react-native';
import { createMaterialTopTabNavigator } from '@react-navigation/material-top-tabs';
import { ThemeToggle } from '@/components/ThemeToggle';
import { SearchPopover } from '@/components/library/SearchPopover';
import { FilterPopover } from '@/components/library/FilterPopover';
import { FilterSheet, type FilterOptions, type SourceType } from '@/components/library/FilterSheet';
import ExercisesScreen from './exercises';
import TemplatesScreen from './templates';
import ProgramsScreen from './programs';
import Header from '@/components/Header';
import { useState } from 'react';
import { useTheme } from '@react-navigation/native';
import type { CustomTheme } from '@/lib/theme';
import { TabScreen } from '@/components/layout/TabScreen';
const Tab = createMaterialTopTabNavigator();
// Default available filters
const availableFilters = {
equipment: ['Barbell', 'Dumbbell', 'Bodyweight', 'Machine', 'Cables', 'Other'],
tags: ['Strength', 'Cardio', 'Mobility', 'Recovery'],
source: ['local', 'powr', 'nostr'] as SourceType[]
};
// Initial filter state
const initialFilters: FilterOptions = {
equipment: [],
tags: [],
source: []
};
export default function LibraryLayout() {
const theme = useTheme() as CustomTheme;
const [searchQuery, setSearchQuery] = useState('');
const [activeFilters, setActiveFilters] = useState(0);
const [filterSheetOpen, setFilterSheetOpen] = useState(false);
const [currentFilters, setCurrentFilters] = useState<FilterOptions>(initialFilters);
const handleApplyFilters = (filters: FilterOptions) => {
setCurrentFilters(filters);
const totalFilters = Object.values(filters).reduce(
(acc, curr) => acc + curr.length,
0
);
setActiveFilters(totalFilters);
};
return (
<TabScreen>
<Header
title="Library"
rightElement={
<View className="flex-row items-center gap-2">
<SearchPopover
searchQuery={searchQuery}
onSearchChange={setSearchQuery}
/>
<FilterPopover
activeFilters={activeFilters}
onOpenFilters={() => setFilterSheetOpen(true)}
/>
<ThemeToggle />
</View>
}
/>
<FilterSheet
isOpen={filterSheetOpen}
onClose={() => setFilterSheetOpen(false)}
options={currentFilters}
onApplyFilters={handleApplyFilters}
availableFilters={availableFilters}
/>
<Header useLogo={true} />
<Tab.Navigator
initialRouteName="templates"

View File

@ -1,22 +1,38 @@
// app/(tabs)/library/exercises.tsx
import React, { useState } from 'react';
import { View, ActivityIndicator } from 'react-native';
import { Text } from '@/components/ui/text';
import { View, ActivityIndicator, ScrollView } from 'react-native';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Search, Dumbbell } from 'lucide-react-native';
import { Search, Dumbbell, ListFilter } from 'lucide-react-native';
import { FloatingActionButton } from '@/components/shared/FloatingActionButton';
import { NewExerciseSheet } from '@/components/library/NewExerciseSheet';
import { SimplifiedExerciseList } from '@/components/exercises/SimplifiedExerciseList';
import { ExerciseDetails } from '@/components/exercises/ExerciseDetails';
import { ExerciseDisplay, ExerciseType, BaseExercise } from '@/types/exercise';
import { ExerciseDisplay, ExerciseType, BaseExercise, Equipment } from '@/types/exercise';
import { useExercises } from '@/lib/hooks/useExercises';
import { FilterSheet, type FilterOptions, type SourceType } from '@/components/library/FilterSheet';
// Default available filters
const availableFilters = {
equipment: ['Barbell', 'Dumbbell', 'Bodyweight', 'Machine', 'Cables', 'Other'],
tags: ['Strength', 'Cardio', 'Mobility', 'Recovery'],
source: ['local', 'powr', 'nostr'] as SourceType[]
};
// Initial filter state
const initialFilters: FilterOptions = {
equipment: [],
tags: [],
source: []
};
export default function ExercisesScreen() {
const [showNewExercise, setShowNewExercise] = useState(false);
const [searchQuery, setSearchQuery] = useState('');
const [activeFilter, setActiveFilter] = useState<ExerciseType | null>(null);
const [selectedExercise, setSelectedExercise] = useState<ExerciseDisplay | null>(null);
const [filterSheetOpen, setFilterSheetOpen] = useState(false);
const [currentFilters, setCurrentFilters] = useState<FilterOptions>(initialFilters);
const [activeFilters, setActiveFilters] = useState(0);
const {
exercises,
@ -38,15 +54,6 @@ export default function ExercisesScreen() {
}
}, [searchQuery, updateFilters]);
// Update type filter when activeFilter changes
React.useEffect(() => {
if (activeFilter) {
updateFilters({ type: [activeFilter] });
} else {
clearFilters();
}
}, [activeFilter, updateFilters, clearFilters]);
const handleExercisePress = (exercise: ExerciseDisplay) => {
setSelectedExercise(exercise);
};
@ -68,45 +75,79 @@ export default function ExercisesScreen() {
await createExercise(exerciseWithSource);
setShowNewExercise(false);
};
if (loading) {
return (
<View className="flex-1 items-center justify-center bg-background">
<ActivityIndicator size="large" color="#8B5CF6" />
<Text className="mt-4 text-muted-foreground">Loading exercises...</Text>
</View>
);
}
if (error) {
return (
<View className="flex-1 items-center justify-center p-4 bg-background">
<Text className="text-destructive text-center mb-4">
{error.message}
</Text>
<Button onPress={refreshExercises}>
<Text className="text-primary-foreground">Retry</Text>
</Button>
</View>
const handleApplyFilters = (filters: FilterOptions) => {
setCurrentFilters(filters);
const totalFilters = Object.values(filters).reduce(
(acc, curr) => acc + curr.length,
0
);
}
setActiveFilters(totalFilters);
// Update the exercises hook filters with proper type casting
if (filters.equipment.length > 0) {
// Convert string[] to Equipment[]
const typedEquipment = filters.equipment.filter(eq =>
['bodyweight', 'barbell', 'dumbbell', 'kettlebell', 'machine', 'cable', 'other'].includes(eq.toLowerCase())
) as Equipment[];
updateFilters({ equipment: typedEquipment });
}
if (filters.tags.length > 0) {
updateFilters({ tags: filters.tags });
}
if (filters.source.length > 0) {
updateFilters({ source: filters.source as any[] });
}
if (totalFilters === 0) {
clearFilters();
}
};
return (
<View className="flex-1 bg-background">
{/* Search bar */}
<View className="px-4 py-3">
<View className="relative flex-row items-center bg-muted rounded-xl">
<View className="absolute left-3 z-10">
<Search size={18} className="text-muted-foreground" />
{/* Search bar with filter button */}
<View className="px-4 py-2 border-b border-border">
<View className="flex-row items-center">
<View className="relative flex-1">
<View className="absolute left-3 z-10 h-full justify-center">
<Search size={18} className="text-muted-foreground" />
</View>
<Input
value={searchQuery}
onChangeText={setSearchQuery}
placeholder="Search exercises"
className="pl-9 pr-10 bg-muted/50 border-0"
/>
<View className="absolute right-2 z-10 h-full justify-center">
<Button
variant="ghost"
size="icon"
onPress={() => setFilterSheetOpen(true)}
>
<View className="relative">
<ListFilter className="text-muted-foreground" size={20} />
{activeFilters > 0 && (
<View className="absolute -top-1 -right-1 w-2.5 h-2.5 rounded-full" style={{ backgroundColor: '#f7931a' }} />
)}
</View>
</Button>
</View>
</View>
<Input
value={searchQuery}
onChangeText={setSearchQuery}
placeholder="Search"
className="pl-9 bg-transparent h-10 flex-1"
/>
</View>
</View>
{/* Filter Sheet */}
<FilterSheet
isOpen={filterSheetOpen}
onClose={() => setFilterSheetOpen(false)}
options={currentFilters}
onApplyFilters={handleApplyFilters}
availableFilters={availableFilters}
/>
{/* Exercises list */}
<SimplifiedExerciseList
exercises={exercises}

View File

@ -3,13 +3,15 @@ import React, { useState, useEffect } from 'react';
import { View, ScrollView } from 'react-native';
import { Text } from '@/components/ui/text';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { AlertCircle, CheckCircle2, Database, RefreshCcw, Trash2, Code } from 'lucide-react-native';
import { AlertCircle, CheckCircle2, Database, RefreshCcw, Trash2, Code, Search, ListFilter } from 'lucide-react-native';
import { useSQLiteContext } from 'expo-sqlite';
import { ExerciseType, ExerciseCategory, Equipment } from '@/types/exercise';
import { SQLTransaction, SQLResultSet, SQLError } from '@/lib/db/types';
import { schema } from '@/lib/db/schema';
import { Platform } from 'react-native';
import { FilterSheet, type FilterOptions, type SourceType } from '@/components/library/FilterSheet';
interface TableInfo {
name: string;
@ -37,6 +39,20 @@ interface ExerciseRow {
format_units_json: string;
}
// Default available filters for programs - can be adjusted later
const availableFilters = {
equipment: ['Barbell', 'Dumbbell', 'Bodyweight', 'Machine', 'Cables', 'Other'],
tags: ['Strength', 'Cardio', 'Mobility', 'Recovery'],
source: ['local', 'powr', 'nostr'] as SourceType[]
};
// Initial filter state
const initialFilters: FilterOptions = {
equipment: [],
tags: [],
source: []
};
export default function ProgramsScreen() {
const db = useSQLiteContext();
const [dbStatus, setDbStatus] = useState<{
@ -52,6 +68,10 @@ export default function ProgramsScreen() {
success: boolean;
message: string;
} | null>(null);
const [searchQuery, setSearchQuery] = useState('');
const [filterSheetOpen, setFilterSheetOpen] = useState(false);
const [currentFilters, setCurrentFilters] = useState<FilterOptions>(initialFilters);
const [activeFilters, setActiveFilters] = useState(0);
useEffect(() => {
checkDatabase();
@ -200,132 +220,189 @@ export default function ProgramsScreen() {
}
};
return (
<ScrollView className="flex-1 bg-background p-4">
<View className="py-4 space-y-4">
<Text className="text-lg font-semibold text-center mb-4">Database Debug Panel</Text>
{/* Schema Inspector Card */}
<Card>
<CardHeader>
<CardTitle className="flex-row items-center gap-2">
<Code size={20} className="text-foreground" />
<Text className="text-lg font-semibold">Database Schema ({Platform.OS})</Text>
</CardTitle>
</CardHeader>
<CardContent>
<View className="space-y-4">
{schemas.map((table) => (
<View key={table.name} className="space-y-2">
<Text className="font-semibold">{table.name}</Text>
<Text className="text-muted-foreground text-sm">
{table.sql}
</Text>
</View>
))}
</View>
<Button
className="mt-4"
onPress={inspectDatabase}
>
<Text className="text-primary-foreground">Refresh Schema</Text>
</Button>
</CardContent>
</Card>
const handleApplyFilters = (filters: FilterOptions) => {
setCurrentFilters(filters);
const totalFilters = Object.values(filters).reduce(
(acc, curr) => acc + curr.length,
0
);
setActiveFilters(totalFilters);
// Implement filtering logic for programs when available
};
{/* Status Card */}
<Card>
<CardHeader>
<CardTitle className="flex-row items-center gap-2">
<Database size={20} className="text-foreground" />
<Text className="text-lg font-semibold">Database Status</Text>
</CardTitle>
</CardHeader>
<CardContent>
<View className="space-y-2">
<Text>Initialized: {dbStatus.initialized ? '✅' : '❌'}</Text>
<Text>Tables Found: {dbStatus.tables.length}</Text>
<View className="pl-4">
{dbStatus.tables.map(table => (
<Text key={table} className="text-muted-foreground"> {table}</Text>
return (
<View className="flex-1 bg-background">
{/* Search bar with filter button */}
<View className="px-4 py-2 border-b border-border">
<View className="flex-row items-center">
<View className="relative flex-1">
<View className="absolute left-3 z-10 h-full justify-center">
<Search size={18} className="text-muted-foreground" />
</View>
<Input
value={searchQuery}
onChangeText={setSearchQuery}
placeholder="Search programs"
className="pl-9 pr-10 bg-muted/50 border-0"
/>
<View className="absolute right-2 z-10 h-full justify-center">
<Button
variant="ghost"
size="icon"
onPress={() => setFilterSheetOpen(true)}
>
<View className="relative">
<ListFilter className="text-muted-foreground" size={20} />
{activeFilters > 0 && (
<View className="absolute -top-1 -right-1 w-2.5 h-2.5 rounded-full" style={{ backgroundColor: '#f7931a' }} />
)}
</View>
</Button>
</View>
</View>
</View>
</View>
{/* Filter Sheet */}
<FilterSheet
isOpen={filterSheetOpen}
onClose={() => setFilterSheetOpen(false)}
options={currentFilters}
onApplyFilters={handleApplyFilters}
availableFilters={availableFilters}
/>
<ScrollView className="flex-1 p-4">
<View className="py-4 space-y-4">
<Text className="text-lg font-semibold text-center mb-4">Programs Coming Soon</Text>
<Text className="text-center text-muted-foreground mb-6">
Training programs will allow you to organize your workouts into structured training plans.
</Text>
<Text className="text-lg font-semibold text-center mb-4">Database Debug Panel</Text>
{/* Schema Inspector Card */}
<Card>
<CardHeader>
<CardTitle className="flex-row items-center gap-2">
<Code size={20} className="text-foreground" />
<Text className="text-lg font-semibold">Database Schema ({Platform.OS})</Text>
</CardTitle>
</CardHeader>
<CardContent>
<View className="space-y-4">
{schemas.map((table) => (
<View key={table.name} className="space-y-2">
<Text className="font-semibold">{table.name}</Text>
<Text className="text-muted-foreground text-sm">
{table.sql}
</Text>
</View>
))}
</View>
{dbStatus.error && (
<View className="mt-4 p-4 bg-destructive/10 rounded-lg border border-destructive">
<View className="flex-row items-center gap-2">
<AlertCircle className="text-destructive" size={20} />
<Text className="font-semibold text-destructive">Error</Text>
</View>
<Text className="mt-2 text-destructive">{dbStatus.error}</Text>
</View>
)}
</View>
</CardContent>
</Card>
<Button
className="mt-4"
onPress={inspectDatabase}
>
<Text className="text-primary-foreground">Refresh Schema</Text>
</Button>
</CardContent>
</Card>
{/* Operations Card */}
<Card>
<CardHeader>
<CardTitle className="flex-row items-center gap-2">
<RefreshCcw size={20} className="text-foreground" />
<Text className="text-lg font-semibold">Database Operations</Text>
</CardTitle>
</CardHeader>
<CardContent>
<View className="space-y-4">
<Button
onPress={runTestInsert}
className="w-full"
>
<Text className="text-primary-foreground">Run Test Insert</Text>
</Button>
<Button
onPress={resetDatabase}
variant="destructive"
className="w-full"
>
<Trash2 size={18} className="mr-2" />
<Text className="text-destructive-foreground">Reset Database</Text>
</Button>
{testResults && (
<View className={`mt-4 p-4 rounded-lg border ${
testResults.success
? 'bg-primary/10 border-primary'
: 'bg-destructive/10 border-destructive'
}`}>
<View className="flex-row items-center gap-2">
{testResults.success ? (
<CheckCircle2
className="text-primary"
size={20}
/>
) : (
<AlertCircle
className="text-destructive"
size={20}
/>
)}
<Text className={`font-semibold ${
testResults.success ? 'text-primary' : 'text-destructive'
}`}>
{testResults.success ? "Success" : "Error"}
</Text>
</View>
<ScrollView className="mt-2">
<Text className={`${
testResults.success ? 'text-foreground' : 'text-destructive'
}`}>
{testResults.message}
</Text>
</ScrollView>
{/* Status Card */}
<Card>
<CardHeader>
<CardTitle className="flex-row items-center gap-2">
<Database size={20} className="text-foreground" />
<Text className="text-lg font-semibold">Database Status</Text>
</CardTitle>
</CardHeader>
<CardContent>
<View className="space-y-2">
<Text>Initialized: {dbStatus.initialized ? '✅' : '❌'}</Text>
<Text>Tables Found: {dbStatus.tables.length}</Text>
<View className="pl-4">
{dbStatus.tables.map(table => (
<Text key={table} className="text-muted-foreground"> {table}</Text>
))}
</View>
)}
</View>
</CardContent>
</Card>
</View>
</ScrollView>
{dbStatus.error && (
<View className="mt-4 p-4 bg-destructive/10 rounded-lg border border-destructive">
<View className="flex-row items-center gap-2">
<AlertCircle className="text-destructive" size={20} />
<Text className="font-semibold text-destructive">Error</Text>
</View>
<Text className="mt-2 text-destructive">{dbStatus.error}</Text>
</View>
)}
</View>
</CardContent>
</Card>
{/* Operations Card */}
<Card>
<CardHeader>
<CardTitle className="flex-row items-center gap-2">
<RefreshCcw size={20} className="text-foreground" />
<Text className="text-lg font-semibold">Database Operations</Text>
</CardTitle>
</CardHeader>
<CardContent>
<View className="space-y-4">
<Button
onPress={runTestInsert}
className="w-full"
>
<Text className="text-primary-foreground">Run Test Insert</Text>
</Button>
<Button
onPress={resetDatabase}
variant="destructive"
className="w-full"
>
<Trash2 size={18} className="mr-2" />
<Text className="text-destructive-foreground">Reset Database</Text>
</Button>
{testResults && (
<View className={`mt-4 p-4 rounded-lg border ${
testResults.success
? 'bg-primary/10 border-primary'
: 'bg-destructive/10 border-destructive'
}`}>
<View className="flex-row items-center gap-2">
{testResults.success ? (
<CheckCircle2
className="text-primary"
size={20}
/>
) : (
<AlertCircle
className="text-destructive"
size={20}
/>
)}
<Text className={`font-semibold ${
testResults.success ? 'text-primary' : 'text-destructive'
}`}>
{testResults.success ? "Success" : "Error"}
</Text>
</View>
<ScrollView className="mt-2">
<Text className={`${
testResults.success ? 'text-foreground' : 'text-destructive'
}`}>
{testResults.message}
</Text>
</ScrollView>
</View>
)}
</View>
</CardContent>
</Card>
</View>
</ScrollView>
</View>
);
}

View File

@ -1,15 +1,16 @@
// app/(tabs)/library/templates.tsx
import React, { useState } from 'react';
import { View, ScrollView } from 'react-native';
import { router } from 'expo-router'; // Add this import
import { router } from 'expo-router';
import { Text } from '@/components/ui/text';
import { Input } from '@/components/ui/input';
import { useFocusEffect } from '@react-navigation/native';
import { Search, Plus } from 'lucide-react-native';
import { Search, Plus, ListFilter } from 'lucide-react-native';
import { FloatingActionButton } from '@/components/shared/FloatingActionButton';
import { NewTemplateSheet } from '@/components/library/NewTemplateSheet';
import { FilterSheet, type FilterOptions, type SourceType } from '@/components/library/FilterSheet';
import { TemplateCard } from '@/components/templates/TemplateCard';
// Remove TemplateDetails import since we're not using it anymore
import { Button } from '@/components/ui/button';
import {
Template,
TemplateCategory,
@ -17,13 +18,19 @@ import {
} from '@/types/templates';
import { useWorkoutStore } from '@/stores/workoutStore';
const TEMPLATE_CATEGORIES: TemplateCategory[] = [
'Full Body',
'Push/Pull/Legs',
'Upper/Lower',
'Conditioning',
'Custom'
];
// Default available filters
const availableFilters = {
equipment: ['Barbell', 'Dumbbell', 'Bodyweight', 'Machine', 'Cables', 'Other'],
tags: ['Strength', 'Cardio', 'Mobility', 'Recovery'],
source: ['local', 'powr', 'nostr'] as SourceType[]
};
// Initial filter state
const initialFilters: FilterOptions = {
equipment: [],
tags: [],
source: []
};
// Mock data - move to a separate file later
const initialTemplates: Template[] = [
@ -61,14 +68,14 @@ export default function TemplatesScreen() {
const [showNewTemplate, setShowNewTemplate] = useState(false);
const [templates, setTemplates] = useState(initialTemplates);
const [searchQuery, setSearchQuery] = useState('');
const [activeCategory, setActiveCategory] = useState<TemplateCategory | null>(null);
// Remove selectedTemplate state since we're not using it anymore
const [filterSheetOpen, setFilterSheetOpen] = useState(false);
const [currentFilters, setCurrentFilters] = useState<FilterOptions>(initialFilters);
const [activeFilters, setActiveFilters] = useState(0);
const handleDelete = (id: string) => {
setTemplates(current => current.filter(t => t.id !== id));
};
// Update to navigate to the template details screen
const handleTemplatePress = (template: Template) => {
router.push(`/template/${template.id}`);
};
@ -109,6 +116,15 @@ export default function TemplatesScreen() {
}
};
const handleApplyFilters = (filters: FilterOptions) => {
setCurrentFilters(filters);
const totalFilters = Object.values(filters).reduce(
(acc, curr) => acc + curr.length,
0
);
setActiveFilters(totalFilters);
};
useFocusEffect(
React.useCallback(() => {
// Refresh template favorite status when tab gains focus
@ -125,12 +141,29 @@ export default function TemplatesScreen() {
setShowNewTemplate(false);
};
// Filter templates based on category and search
// Filter templates based on search and applied filters
const filteredTemplates = templates.filter(template => {
const matchesCategory = !activeCategory || template.category === activeCategory;
// Filter by search query
const matchesSearch = !searchQuery ||
template.title.toLowerCase().includes(searchQuery.toLowerCase());
return matchesCategory && matchesSearch;
// Filter by equipment if any selected
const matchesEquipment = currentFilters.equipment.length === 0 ||
(template.exercises.some(ex =>
currentFilters.equipment.includes(ex.equipment || '')
));
// Filter by tags if any selected
const matchesTags = currentFilters.tags.length === 0 ||
(template.tags && template.tags.some(tag =>
currentFilters.tags.includes(tag)
));
// Filter by source if any selected
const matchesSource = currentFilters.source.length === 0 ||
currentFilters.source.includes(template.source as SourceType);
return matchesSearch && matchesEquipment && matchesTags && matchesSource;
});
// Separate favorites and regular templates
@ -139,60 +172,52 @@ export default function TemplatesScreen() {
return (
<View className="flex-1 bg-background">
{/* Search bar */}
<View className="px-4 py-2">
<View className="relative flex-row items-center bg-muted rounded-xl">
<View className="absolute left-3 z-10">
<Search size={18} className="text-muted-foreground" />
{/* Search bar with filter button */}
<View className="px-4 py-2 border-b border-border">
<View className="flex-row items-center">
<View className="relative flex-1">
<View className="absolute left-3 z-10 h-full justify-center">
<Search size={18} className="text-muted-foreground" />
</View>
<Input
value={searchQuery}
onChangeText={setSearchQuery}
placeholder="Search templates"
className="pl-9 pr-10 bg-muted/50 border-0"
/>
<View className="absolute right-2 z-10 h-full justify-center">
<Button
variant="ghost"
size="icon"
onPress={() => setFilterSheetOpen(true)}
>
<View className="relative">
<ListFilter className="text-muted-foreground" size={20} />
{activeFilters > 0 && (
<View className="absolute -top-1 -right-1 w-2.5 h-2.5 rounded-full" style={{ backgroundColor: '#f7931a' }} />
)}
</View>
</Button>
</View>
</View>
<Input
value={searchQuery}
onChangeText={setSearchQuery}
placeholder="Search templates"
className="pl-9 bg-transparent h-10 flex-1"
/>
</View>
</View>
{/* // Category filters
<ScrollView
horizontal
showsHorizontalScrollIndicator={false}
className="px-4 py-2 border-b border-border"
>
<View className="flex-row gap-2">
<Button
variant={activeCategory === null ? "default" : "outline"}
size="sm"
className="rounded-full"
onPress={() => setActiveCategory(null)}
>
<Text className={activeCategory === null ? "text-primary-foreground" : ""}>
All
</Text>
</Button>
{TEMPLATE_CATEGORIES.map((category) => (
<Button
key={category}
variant={activeCategory === category ? "default" : "outline"}
size="sm"
className="rounded-full"
onPress={() => setActiveCategory(activeCategory === category ? null : category)}
>
<Text className={activeCategory === category ? "text-primary-foreground" : ""}>
{category}
</Text>
</Button>
))}
</View>
</ScrollView> */}
{/* Filter Sheet */}
<FilterSheet
isOpen={filterSheetOpen}
onClose={() => setFilterSheetOpen(false)}
options={currentFilters}
onApplyFilters={handleApplyFilters}
availableFilters={availableFilters}
/>
{/* Templates list */}
<ScrollView>
<ScrollView className="flex-1">
{/* Favorites Section */}
{favoriteTemplates.length > 0 && (
<View>
<Text className="text-lg font-semibold px-4 py-2">
<View className="py-4">
<Text className="text-lg font-semibold mb-4 px-4">
Favorites
</Text>
<View className="gap-3">
@ -211,8 +236,8 @@ export default function TemplatesScreen() {
)}
{/* All Templates Section */}
<View>
<Text className="text-lg font-semibold px-4 py-2">
<View className="py-4">
<Text className="text-lg font-semibold mb-4 px-4">
All Templates
</Text>
{regularTemplates.length > 0 ? (
@ -241,8 +266,6 @@ export default function TemplatesScreen() {
<View className="h-20" />
</ScrollView>
{/* Remove the TemplateDetails component since we're using router navigation now */}
<FloatingActionButton
icon={Plus}
onPress={() => setShowNewTemplate(true)}

View File

@ -1,7 +1,7 @@
// app/(tabs)/profile.tsx
import { View, ScrollView, ImageBackground } from 'react-native';
import { useState, useEffect } from 'react';
import { Settings, LogIn } from 'lucide-react-native';
import { Settings, LogIn, Bell } from 'lucide-react-native';
import { H1 } from '@/components/ui/typography';
import { Button } from '@/components/ui/button';
import { Text } from '@/components/ui/text';
@ -67,7 +67,21 @@ export default function ProfileScreen() {
if (!isAuthenticated) {
return (
<TabScreen>
<Header title="Profile" />
<Header
useLogo={true}
rightElement={
<Button
variant="ghost"
size="icon"
onPress={() => console.log('Open notifications')}
>
<View className="relative">
<Bell className="text-foreground" />
<View className="absolute -top-1 -right-1 w-2 h-2 bg-primary rounded-full" />
</View>
</Button>
}
/>
<View className="flex-1 items-center justify-center p-6">
<View className="items-center mb-8">
<UserAvatar
@ -102,14 +116,17 @@ export default function ProfileScreen() {
return (
<TabScreen>
<Header
title="Profile"
useLogo={true}
rightElement={
<Button
variant="ghost"
size="icon"
onPress={() => console.log('Open settings')}
onPress={() => console.log('Open notifications')}
>
<Settings className="text-foreground" />
<View className="relative">
<Bell className="text-foreground" />
<View className="absolute -top-1 -right-1 w-2 h-2 bg-primary rounded-full" />
</View>
</Button>
}
/>

View File

@ -23,6 +23,9 @@ import OverviewTab from './index';
import SocialTab from './social';
import HistoryTab from './history';
// Import the shared context
import { TemplateContext } from './_templateContext';
// Types
import { WorkoutTemplate } from '@/types/templates';
import type { CustomTheme } from '@/lib/theme';
@ -262,22 +265,4 @@ export default function TemplateDetailsLayout() {
</View>
</TabScreen>
);
}
// Create a context to share the template with the tab screens
interface TemplateContextType {
template: WorkoutTemplate | null;
}
export const TemplateContext = React.createContext<TemplateContextType>({
template: null
});
// Custom hook to access the template
export function useTemplate() {
const context = React.useContext(TemplateContext);
if (!context.template) {
throw new Error('useTemplate must be used within a TemplateContext.Provider');
}
return context.template;
}

View File

@ -0,0 +1,27 @@
// app/(workout)/template/[id]/templateContext.tsx
import React from 'react';
import { WorkoutTemplate } from '@/types/templates';
// Create a context to share the template with the tab screens
interface TemplateContextType {
template: WorkoutTemplate | null;
}
export const TemplateContext = React.createContext<TemplateContextType>({
template: null
});
// Custom hook to access the template
export function useTemplate() {
const context = React.useContext(TemplateContext);
if (!context.template) {
throw new Error('useTemplate must be used within a TemplateContext.Provider');
}
return context.template;
}
// Add a default export to satisfy Expo Router
// The _ prefix in the filename would also work to exclude it from routing
export default function TemplateContextProvider() {
// This component won't actually be used
return null;
}

View File

@ -6,7 +6,7 @@ import { Button } from '@/components/ui/button';
import { Badge } from '@/components/ui/badge';
import { Card, CardContent } from '@/components/ui/card';
import { Calendar } from 'lucide-react-native';
import { useTemplate } from './_layout';
import { useTemplate } from './_templateContext';
// Format date helper
const formatDate = (date: Date) => {

View File

@ -6,7 +6,7 @@ import { Button } from '@/components/ui/button';
import { Badge } from '@/components/ui/badge';
import { Separator } from '@/components/ui/separator';
import { Card, CardContent } from '@/components/ui/card';
import { useTemplate } from './_layout';
import { useTemplate } from './_templateContext';
import { formatTime } from '@/utils/formatTime';
import {
Edit2,

View File

@ -11,7 +11,7 @@ import {
Repeat,
Bookmark
} from 'lucide-react-native';
import { useTemplate } from './_layout';
import { useTemplate } from './_templateContext';
import { cn } from '@/lib/utils';
// Mock social feed data

View File

@ -7,6 +7,7 @@ import { Bell } from 'lucide-react-native';
import { Text } from '@/components/ui/text';
import { Button } from '@/components/ui/button';
import UserAvatar from '@/components/UserAvatar';
import PowerLogo from '@/components/PowerLogo';
import { useSettingsDrawer } from '@/lib/contexts/SettingsDrawerContext';
import { useNDKCurrentUser } from '@/lib/hooks/useNDK';
@ -14,12 +15,14 @@ interface HeaderProps {
title?: string;
hideTitle?: boolean;
rightElement?: React.ReactNode;
useLogo?: boolean;
}
export default function Header({
title,
hideTitle = false,
rightElement
rightElement,
useLogo = false
}: HeaderProps) {
const theme = useTheme();
const insets = useSafeAreaInsets();
@ -56,9 +59,13 @@ export default function Header({
fallback={fallbackLetter}
/>
{/* Middle - Title */}
<View style={styles.titleContainer}>
<Text style={styles.title}>{title}</Text>
{/* Middle - Title or Logo */}
<View style={[styles.titleContainer, { marginLeft: 10 }]}>
{useLogo ? (
<PowerLogo size="md" />
) : (
<Text style={styles.title}>{title}</Text>
)}
</View>
{/* Right side - Custom element or default notifications */}
@ -69,7 +76,11 @@ export default function Header({
size="icon"
onPress={() => {}}
>
<Bell size={24} className="text-foreground" />
<View className="relative">
<Bell size={24} className="text-foreground" />
{/* Notification indicator - you can conditionally render this */}
<View className="absolute -top-1 -right-1 w-2 h-2 bg-primary rounded-full" />
</View>
</Button>
)}
</View>
@ -93,7 +104,9 @@ const styles = StyleSheet.create({
titleContainer: {
flex: 1,
alignItems: 'center',
paddingHorizontal: 16,
justifyContent: 'center',
paddingHorizontal: 0, // Remove padding to allow more precise positioning
height: '100%',
},
title: {
fontSize: 20,

57
components/PowerLogo.tsx Normal file
View File

@ -0,0 +1,57 @@
// components/PowerLogo.tsx
import React from 'react';
import { View, Text as RNText, useColorScheme } from 'react-native';
import { Zap } from 'lucide-react-native';
import { useTheme } from '@react-navigation/native';
interface PowerLogoProps {
size?: 'sm' | 'md' | 'lg';
}
export default function PowerLogo({ size = 'md' }: PowerLogoProps) {
const theme = useTheme();
const colorScheme = useColorScheme();
const fontSize = {
sm: 18,
md: 22,
lg: 26,
}[size];
const iconSize = {
sm: 14,
md: 16,
lg: 20,
}[size];
// Use theme colors to ensure visibility in both light and dark mode
const textColor = theme.colors.primary || (colorScheme === 'dark' ? '#9c5cff' : '#6b21a8');
return (
<View style={{
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'center',
paddingVertical: 4, // Add padding to prevent clipping
}}>
<RNText
style={{
fontWeight: 'bold',
fontStyle: 'italic',
fontSize: fontSize,
color: textColor,
includeFontPadding: false, // Helps with text clipping
textAlignVertical: 'center',
}}
>
POWR
</RNText>
<Zap
size={iconSize}
color="#FFD700" // Gold color for the lightning bolt
fill="#FFD700"
style={{ marginLeft: 2 }}
/>
</View>
);
}

View File

@ -176,48 +176,6 @@ export const SimplifiedExerciseList = ({
}}
/>
</View>
{/* Alphabet List */}
<View
className="w-8 justify-center bg-transparent px-1"
onStartShouldSetResponder={() => true}
onResponderMove={(evt) => {
const touch = evt.nativeEvent;
const element = evt.target;
if (element) {
(element as any).measure((x: number, y: number, width: number, height: number, pageX: number, pageY: number) => {
const totalHeight = height;
const letterHeight = totalHeight / alphabet.length;
const touchY = touch.pageY - pageY;
const index = Math.min(
Math.max(Math.floor(touchY / letterHeight), 0),
alphabet.length - 1
);
const letter = alphabet[index];
if (availableLetters.has(letter)) {
scrollToSection(letter);
}
});
}
}}
>
{alphabet.map((letter) => (
<Text
key={letter}
className={
letter === currentSection
? 'text-xs text-center text-primary font-bold py-0.5'
: availableLetters.has(letter)
? 'text-xs text-center text-primary font-medium py-0.5'
: 'text-xs text-center text-muted-foreground py-0.5'
}
>
{letter}
</Text>
))}
</View>
</View>
);
};

View File

@ -26,6 +26,7 @@ export interface TemplateExerciseDisplay {
title: string;
targetSets: number;
targetReps: number;
equipment?: string;
notes?: string;
}