mirror of
https://github.com/DocNR/POWR.git
synced 2025-05-20 00:42:07 +00:00
updated header in tabs and searchbar in library
This commit is contained in:
parent
0d460e8f3e
commit
15c973f333
@ -1,28 +1,31 @@
|
|||||||
// app/(tabs)/history.tsx
|
// app/(tabs)/social.tsx
|
||||||
import { View } from 'react-native';
|
import { View } from 'react-native';
|
||||||
import { Text } from '@/components/ui/text';
|
import { Text } from '@/components/ui/text';
|
||||||
import { TabScreen } from '@/components/layout/TabScreen';
|
|
||||||
import Header from '@/components/Header';
|
|
||||||
import { Button } from '@/components/ui/button';
|
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 (
|
return (
|
||||||
<TabScreen>
|
<TabScreen>
|
||||||
<Header
|
<Header
|
||||||
title="History"
|
useLogo={true}
|
||||||
rightElement={
|
rightElement={
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="icon"
|
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>
|
</Button>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
<View className="flex-1 items-center justify-center">
|
<View className="flex-1 items-center justify-center">
|
||||||
<Text>History Screen</Text>
|
<Text>Social Screen</Text>
|
||||||
</View>
|
</View>
|
||||||
</TabScreen>
|
</TabScreen>
|
||||||
);
|
);
|
||||||
|
@ -20,6 +20,8 @@ import FavoriteTemplate from '@/components/workout/FavoriteTemplate'
|
|||||||
import { useWorkoutStore } from '@/stores/workoutStore'
|
import { useWorkoutStore } from '@/stores/workoutStore'
|
||||||
import { Text } from '@/components/ui/text'
|
import { Text } from '@/components/ui/text'
|
||||||
import { getRandomWorkoutTitle } from '@/utils/workoutTitles'
|
import { getRandomWorkoutTitle } from '@/utils/workoutTitles'
|
||||||
|
import { Bell } from 'lucide-react-native';
|
||||||
|
import { Button } from '@/components/ui/button';
|
||||||
|
|
||||||
interface FavoriteTemplateData {
|
interface FavoriteTemplateData {
|
||||||
id: string;
|
id: string;
|
||||||
@ -204,7 +206,21 @@ export default function WorkoutScreen() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<TabScreen>
|
<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
|
<ScrollView
|
||||||
className="flex-1 px-4 pt-4"
|
className="flex-1 px-4 pt-4"
|
||||||
|
@ -1,77 +1,22 @@
|
|||||||
// app/(tabs)/library/_layout.tsx
|
// app/(tabs)/library/_layout.tsx
|
||||||
import { View } from 'react-native';
|
import { View } from 'react-native';
|
||||||
import { createMaterialTopTabNavigator } from '@react-navigation/material-top-tabs';
|
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 ExercisesScreen from './exercises';
|
||||||
import TemplatesScreen from './templates';
|
import TemplatesScreen from './templates';
|
||||||
import ProgramsScreen from './programs';
|
import ProgramsScreen from './programs';
|
||||||
import Header from '@/components/Header';
|
import Header from '@/components/Header';
|
||||||
import { useState } from 'react';
|
|
||||||
import { useTheme } from '@react-navigation/native';
|
import { useTheme } from '@react-navigation/native';
|
||||||
import type { CustomTheme } from '@/lib/theme';
|
import type { CustomTheme } from '@/lib/theme';
|
||||||
import { TabScreen } from '@/components/layout/TabScreen';
|
import { TabScreen } from '@/components/layout/TabScreen';
|
||||||
|
|
||||||
const Tab = createMaterialTopTabNavigator();
|
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() {
|
export default function LibraryLayout() {
|
||||||
const theme = useTheme() as CustomTheme;
|
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 (
|
return (
|
||||||
<TabScreen>
|
<TabScreen>
|
||||||
<Header
|
<Header useLogo={true} />
|
||||||
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}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Tab.Navigator
|
<Tab.Navigator
|
||||||
initialRouteName="templates"
|
initialRouteName="templates"
|
||||||
|
@ -1,22 +1,38 @@
|
|||||||
// app/(tabs)/library/exercises.tsx
|
// app/(tabs)/library/exercises.tsx
|
||||||
import React, { useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
import { View, ActivityIndicator } from 'react-native';
|
import { View, ActivityIndicator, ScrollView } from 'react-native';
|
||||||
import { Text } from '@/components/ui/text';
|
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { Input } from '@/components/ui/input';
|
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 { FloatingActionButton } from '@/components/shared/FloatingActionButton';
|
||||||
import { NewExerciseSheet } from '@/components/library/NewExerciseSheet';
|
import { NewExerciseSheet } from '@/components/library/NewExerciseSheet';
|
||||||
import { SimplifiedExerciseList } from '@/components/exercises/SimplifiedExerciseList';
|
import { SimplifiedExerciseList } from '@/components/exercises/SimplifiedExerciseList';
|
||||||
import { ExerciseDetails } from '@/components/exercises/ExerciseDetails';
|
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 { 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() {
|
export default function ExercisesScreen() {
|
||||||
const [showNewExercise, setShowNewExercise] = useState(false);
|
const [showNewExercise, setShowNewExercise] = useState(false);
|
||||||
const [searchQuery, setSearchQuery] = useState('');
|
const [searchQuery, setSearchQuery] = useState('');
|
||||||
const [activeFilter, setActiveFilter] = useState<ExerciseType | null>(null);
|
|
||||||
const [selectedExercise, setSelectedExercise] = useState<ExerciseDisplay | 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 {
|
const {
|
||||||
exercises,
|
exercises,
|
||||||
@ -38,15 +54,6 @@ export default function ExercisesScreen() {
|
|||||||
}
|
}
|
||||||
}, [searchQuery, updateFilters]);
|
}, [searchQuery, updateFilters]);
|
||||||
|
|
||||||
// Update type filter when activeFilter changes
|
|
||||||
React.useEffect(() => {
|
|
||||||
if (activeFilter) {
|
|
||||||
updateFilters({ type: [activeFilter] });
|
|
||||||
} else {
|
|
||||||
clearFilters();
|
|
||||||
}
|
|
||||||
}, [activeFilter, updateFilters, clearFilters]);
|
|
||||||
|
|
||||||
const handleExercisePress = (exercise: ExerciseDisplay) => {
|
const handleExercisePress = (exercise: ExerciseDisplay) => {
|
||||||
setSelectedExercise(exercise);
|
setSelectedExercise(exercise);
|
||||||
};
|
};
|
||||||
@ -68,45 +75,79 @@ export default function ExercisesScreen() {
|
|||||||
await createExercise(exerciseWithSource);
|
await createExercise(exerciseWithSource);
|
||||||
setShowNewExercise(false);
|
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) {
|
const handleApplyFilters = (filters: FilterOptions) => {
|
||||||
return (
|
setCurrentFilters(filters);
|
||||||
<View className="flex-1 items-center justify-center p-4 bg-background">
|
const totalFilters = Object.values(filters).reduce(
|
||||||
<Text className="text-destructive text-center mb-4">
|
(acc, curr) => acc + curr.length,
|
||||||
{error.message}
|
0
|
||||||
</Text>
|
|
||||||
<Button onPress={refreshExercises}>
|
|
||||||
<Text className="text-primary-foreground">Retry</Text>
|
|
||||||
</Button>
|
|
||||||
</View>
|
|
||||||
);
|
);
|
||||||
}
|
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 (
|
return (
|
||||||
<View className="flex-1 bg-background">
|
<View className="flex-1 bg-background">
|
||||||
{/* Search bar */}
|
{/* Search bar with filter button */}
|
||||||
<View className="px-4 py-3">
|
<View className="px-4 py-2 border-b border-border">
|
||||||
<View className="relative flex-row items-center bg-muted rounded-xl">
|
<View className="flex-row items-center">
|
||||||
<View className="absolute left-3 z-10">
|
<View className="relative flex-1">
|
||||||
<Search size={18} className="text-muted-foreground" />
|
<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>
|
</View>
|
||||||
<Input
|
|
||||||
value={searchQuery}
|
|
||||||
onChangeText={setSearchQuery}
|
|
||||||
placeholder="Search"
|
|
||||||
className="pl-9 bg-transparent h-10 flex-1"
|
|
||||||
/>
|
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
|
{/* Filter Sheet */}
|
||||||
|
<FilterSheet
|
||||||
|
isOpen={filterSheetOpen}
|
||||||
|
onClose={() => setFilterSheetOpen(false)}
|
||||||
|
options={currentFilters}
|
||||||
|
onApplyFilters={handleApplyFilters}
|
||||||
|
availableFilters={availableFilters}
|
||||||
|
/>
|
||||||
|
|
||||||
{/* Exercises list */}
|
{/* Exercises list */}
|
||||||
<SimplifiedExerciseList
|
<SimplifiedExerciseList
|
||||||
exercises={exercises}
|
exercises={exercises}
|
||||||
|
@ -3,13 +3,15 @@ import React, { useState, useEffect } from 'react';
|
|||||||
import { View, ScrollView } from 'react-native';
|
import { View, ScrollView } from 'react-native';
|
||||||
import { Text } from '@/components/ui/text';
|
import { Text } from '@/components/ui/text';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
|
import { Input } from '@/components/ui/input';
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
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 { useSQLiteContext } from 'expo-sqlite';
|
||||||
import { ExerciseType, ExerciseCategory, Equipment } from '@/types/exercise';
|
import { ExerciseType, ExerciseCategory, Equipment } from '@/types/exercise';
|
||||||
import { SQLTransaction, SQLResultSet, SQLError } from '@/lib/db/types';
|
import { SQLTransaction, SQLResultSet, SQLError } from '@/lib/db/types';
|
||||||
import { schema } from '@/lib/db/schema';
|
import { schema } from '@/lib/db/schema';
|
||||||
import { Platform } from 'react-native';
|
import { Platform } from 'react-native';
|
||||||
|
import { FilterSheet, type FilterOptions, type SourceType } from '@/components/library/FilterSheet';
|
||||||
|
|
||||||
interface TableInfo {
|
interface TableInfo {
|
||||||
name: string;
|
name: string;
|
||||||
@ -37,6 +39,20 @@ interface ExerciseRow {
|
|||||||
format_units_json: string;
|
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() {
|
export default function ProgramsScreen() {
|
||||||
const db = useSQLiteContext();
|
const db = useSQLiteContext();
|
||||||
const [dbStatus, setDbStatus] = useState<{
|
const [dbStatus, setDbStatus] = useState<{
|
||||||
@ -52,6 +68,10 @@ export default function ProgramsScreen() {
|
|||||||
success: boolean;
|
success: boolean;
|
||||||
message: string;
|
message: string;
|
||||||
} | null>(null);
|
} | null>(null);
|
||||||
|
const [searchQuery, setSearchQuery] = useState('');
|
||||||
|
const [filterSheetOpen, setFilterSheetOpen] = useState(false);
|
||||||
|
const [currentFilters, setCurrentFilters] = useState<FilterOptions>(initialFilters);
|
||||||
|
const [activeFilters, setActiveFilters] = useState(0);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
checkDatabase();
|
checkDatabase();
|
||||||
@ -200,132 +220,189 @@ export default function ProgramsScreen() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
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
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ScrollView className="flex-1 bg-background p-4">
|
<View className="flex-1 bg-background">
|
||||||
<View className="py-4 space-y-4">
|
{/* Search bar with filter button */}
|
||||||
<Text className="text-lg font-semibold text-center mb-4">Database Debug Panel</Text>
|
<View className="px-4 py-2 border-b border-border">
|
||||||
|
<View className="flex-row items-center">
|
||||||
{/* Schema Inspector Card */}
|
<View className="relative flex-1">
|
||||||
<Card>
|
<View className="absolute left-3 z-10 h-full justify-center">
|
||||||
<CardHeader>
|
<Search size={18} className="text-muted-foreground" />
|
||||||
<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>
|
</View>
|
||||||
<Button
|
<Input
|
||||||
className="mt-4"
|
value={searchQuery}
|
||||||
onPress={inspectDatabase}
|
onChangeText={setSearchQuery}
|
||||||
>
|
placeholder="Search programs"
|
||||||
<Text className="text-primary-foreground">Refresh Schema</Text>
|
className="pl-9 pr-10 bg-muted/50 border-0"
|
||||||
</Button>
|
/>
|
||||||
</CardContent>
|
<View className="absolute right-2 z-10 h-full justify-center">
|
||||||
</Card>
|
<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>
|
||||||
|
|
||||||
{/* Status Card */}
|
{/* Filter Sheet */}
|
||||||
<Card>
|
<FilterSheet
|
||||||
<CardHeader>
|
isOpen={filterSheetOpen}
|
||||||
<CardTitle className="flex-row items-center gap-2">
|
onClose={() => setFilterSheetOpen(false)}
|
||||||
<Database size={20} className="text-foreground" />
|
options={currentFilters}
|
||||||
<Text className="text-lg font-semibold">Database Status</Text>
|
onApplyFilters={handleApplyFilters}
|
||||||
</CardTitle>
|
availableFilters={availableFilters}
|
||||||
</CardHeader>
|
/>
|
||||||
<CardContent>
|
|
||||||
<View className="space-y-2">
|
<ScrollView className="flex-1 p-4">
|
||||||
<Text>Initialized: {dbStatus.initialized ? '✅' : '❌'}</Text>
|
<View className="py-4 space-y-4">
|
||||||
<Text>Tables Found: {dbStatus.tables.length}</Text>
|
<Text className="text-lg font-semibold text-center mb-4">Programs Coming Soon</Text>
|
||||||
<View className="pl-4">
|
<Text className="text-center text-muted-foreground mb-6">
|
||||||
{dbStatus.tables.map(table => (
|
Training programs will allow you to organize your workouts into structured training plans.
|
||||||
<Text key={table} className="text-muted-foreground">• {table}</Text>
|
</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>
|
</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>
|
|
||||||
|
|
||||||
{/* 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
|
<Button
|
||||||
onPress={runTestInsert}
|
className="mt-4"
|
||||||
className="w-full"
|
onPress={inspectDatabase}
|
||||||
>
|
>
|
||||||
<Text className="text-primary-foreground">Run Test Insert</Text>
|
<Text className="text-primary-foreground">Refresh Schema</Text>
|
||||||
</Button>
|
</Button>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
<Button
|
{/* Status Card */}
|
||||||
onPress={resetDatabase}
|
<Card>
|
||||||
variant="destructive"
|
<CardHeader>
|
||||||
className="w-full"
|
<CardTitle className="flex-row items-center gap-2">
|
||||||
>
|
<Database size={20} className="text-foreground" />
|
||||||
<Trash2 size={18} className="mr-2" />
|
<Text className="text-lg font-semibold">Database Status</Text>
|
||||||
<Text className="text-destructive-foreground">Reset Database</Text>
|
</CardTitle>
|
||||||
</Button>
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
{testResults && (
|
<View className="space-y-2">
|
||||||
<View className={`mt-4 p-4 rounded-lg border ${
|
<Text>Initialized: {dbStatus.initialized ? '✅' : '❌'}</Text>
|
||||||
testResults.success
|
<Text>Tables Found: {dbStatus.tables.length}</Text>
|
||||||
? 'bg-primary/10 border-primary'
|
<View className="pl-4">
|
||||||
: 'bg-destructive/10 border-destructive'
|
{dbStatus.tables.map(table => (
|
||||||
}`}>
|
<Text key={table} className="text-muted-foreground">• {table}</Text>
|
||||||
<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>
|
||||||
)}
|
{dbStatus.error && (
|
||||||
</View>
|
<View className="mt-4 p-4 bg-destructive/10 rounded-lg border border-destructive">
|
||||||
</CardContent>
|
<View className="flex-row items-center gap-2">
|
||||||
</Card>
|
<AlertCircle className="text-destructive" size={20} />
|
||||||
</View>
|
<Text className="font-semibold text-destructive">Error</Text>
|
||||||
</ScrollView>
|
</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>
|
||||||
);
|
);
|
||||||
}
|
}
|
@ -1,15 +1,16 @@
|
|||||||
// app/(tabs)/library/templates.tsx
|
// app/(tabs)/library/templates.tsx
|
||||||
import React, { useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
import { View, ScrollView } from 'react-native';
|
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 { Text } from '@/components/ui/text';
|
||||||
import { Input } from '@/components/ui/input';
|
import { Input } from '@/components/ui/input';
|
||||||
import { useFocusEffect } from '@react-navigation/native';
|
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 { FloatingActionButton } from '@/components/shared/FloatingActionButton';
|
||||||
import { NewTemplateSheet } from '@/components/library/NewTemplateSheet';
|
import { NewTemplateSheet } from '@/components/library/NewTemplateSheet';
|
||||||
|
import { FilterSheet, type FilterOptions, type SourceType } from '@/components/library/FilterSheet';
|
||||||
import { TemplateCard } from '@/components/templates/TemplateCard';
|
import { TemplateCard } from '@/components/templates/TemplateCard';
|
||||||
// Remove TemplateDetails import since we're not using it anymore
|
import { Button } from '@/components/ui/button';
|
||||||
import {
|
import {
|
||||||
Template,
|
Template,
|
||||||
TemplateCategory,
|
TemplateCategory,
|
||||||
@ -17,13 +18,19 @@ import {
|
|||||||
} from '@/types/templates';
|
} from '@/types/templates';
|
||||||
import { useWorkoutStore } from '@/stores/workoutStore';
|
import { useWorkoutStore } from '@/stores/workoutStore';
|
||||||
|
|
||||||
const TEMPLATE_CATEGORIES: TemplateCategory[] = [
|
// Default available filters
|
||||||
'Full Body',
|
const availableFilters = {
|
||||||
'Push/Pull/Legs',
|
equipment: ['Barbell', 'Dumbbell', 'Bodyweight', 'Machine', 'Cables', 'Other'],
|
||||||
'Upper/Lower',
|
tags: ['Strength', 'Cardio', 'Mobility', 'Recovery'],
|
||||||
'Conditioning',
|
source: ['local', 'powr', 'nostr'] as SourceType[]
|
||||||
'Custom'
|
};
|
||||||
];
|
|
||||||
|
// Initial filter state
|
||||||
|
const initialFilters: FilterOptions = {
|
||||||
|
equipment: [],
|
||||||
|
tags: [],
|
||||||
|
source: []
|
||||||
|
};
|
||||||
|
|
||||||
// Mock data - move to a separate file later
|
// Mock data - move to a separate file later
|
||||||
const initialTemplates: Template[] = [
|
const initialTemplates: Template[] = [
|
||||||
@ -61,14 +68,14 @@ export default function TemplatesScreen() {
|
|||||||
const [showNewTemplate, setShowNewTemplate] = useState(false);
|
const [showNewTemplate, setShowNewTemplate] = useState(false);
|
||||||
const [templates, setTemplates] = useState(initialTemplates);
|
const [templates, setTemplates] = useState(initialTemplates);
|
||||||
const [searchQuery, setSearchQuery] = useState('');
|
const [searchQuery, setSearchQuery] = useState('');
|
||||||
const [activeCategory, setActiveCategory] = useState<TemplateCategory | null>(null);
|
const [filterSheetOpen, setFilterSheetOpen] = useState(false);
|
||||||
// Remove selectedTemplate state since we're not using it anymore
|
const [currentFilters, setCurrentFilters] = useState<FilterOptions>(initialFilters);
|
||||||
|
const [activeFilters, setActiveFilters] = useState(0);
|
||||||
|
|
||||||
const handleDelete = (id: string) => {
|
const handleDelete = (id: string) => {
|
||||||
setTemplates(current => current.filter(t => t.id !== id));
|
setTemplates(current => current.filter(t => t.id !== id));
|
||||||
};
|
};
|
||||||
|
|
||||||
// Update to navigate to the template details screen
|
|
||||||
const handleTemplatePress = (template: Template) => {
|
const handleTemplatePress = (template: Template) => {
|
||||||
router.push(`/template/${template.id}`);
|
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(
|
useFocusEffect(
|
||||||
React.useCallback(() => {
|
React.useCallback(() => {
|
||||||
// Refresh template favorite status when tab gains focus
|
// Refresh template favorite status when tab gains focus
|
||||||
@ -125,12 +141,29 @@ export default function TemplatesScreen() {
|
|||||||
setShowNewTemplate(false);
|
setShowNewTemplate(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
// Filter templates based on category and search
|
// Filter templates based on search and applied filters
|
||||||
const filteredTemplates = templates.filter(template => {
|
const filteredTemplates = templates.filter(template => {
|
||||||
const matchesCategory = !activeCategory || template.category === activeCategory;
|
// Filter by search query
|
||||||
const matchesSearch = !searchQuery ||
|
const matchesSearch = !searchQuery ||
|
||||||
template.title.toLowerCase().includes(searchQuery.toLowerCase());
|
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
|
// Separate favorites and regular templates
|
||||||
@ -139,60 +172,52 @@ export default function TemplatesScreen() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<View className="flex-1 bg-background">
|
<View className="flex-1 bg-background">
|
||||||
{/* Search bar */}
|
{/* Search bar with filter button */}
|
||||||
<View className="px-4 py-2">
|
<View className="px-4 py-2 border-b border-border">
|
||||||
<View className="relative flex-row items-center bg-muted rounded-xl">
|
<View className="flex-row items-center">
|
||||||
<View className="absolute left-3 z-10">
|
<View className="relative flex-1">
|
||||||
<Search size={18} className="text-muted-foreground" />
|
<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>
|
</View>
|
||||||
<Input
|
|
||||||
value={searchQuery}
|
|
||||||
onChangeText={setSearchQuery}
|
|
||||||
placeholder="Search templates"
|
|
||||||
className="pl-9 bg-transparent h-10 flex-1"
|
|
||||||
/>
|
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
{/* // Category filters
|
{/* Filter Sheet */}
|
||||||
<ScrollView
|
<FilterSheet
|
||||||
horizontal
|
isOpen={filterSheetOpen}
|
||||||
showsHorizontalScrollIndicator={false}
|
onClose={() => setFilterSheetOpen(false)}
|
||||||
className="px-4 py-2 border-b border-border"
|
options={currentFilters}
|
||||||
>
|
onApplyFilters={handleApplyFilters}
|
||||||
<View className="flex-row gap-2">
|
availableFilters={availableFilters}
|
||||||
<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> */}
|
|
||||||
|
|
||||||
{/* Templates list */}
|
{/* Templates list */}
|
||||||
<ScrollView>
|
<ScrollView className="flex-1">
|
||||||
{/* Favorites Section */}
|
{/* Favorites Section */}
|
||||||
{favoriteTemplates.length > 0 && (
|
{favoriteTemplates.length > 0 && (
|
||||||
<View>
|
<View className="py-4">
|
||||||
<Text className="text-lg font-semibold px-4 py-2">
|
<Text className="text-lg font-semibold mb-4 px-4">
|
||||||
Favorites
|
Favorites
|
||||||
</Text>
|
</Text>
|
||||||
<View className="gap-3">
|
<View className="gap-3">
|
||||||
@ -211,8 +236,8 @@ export default function TemplatesScreen() {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{/* All Templates Section */}
|
{/* All Templates Section */}
|
||||||
<View>
|
<View className="py-4">
|
||||||
<Text className="text-lg font-semibold px-4 py-2">
|
<Text className="text-lg font-semibold mb-4 px-4">
|
||||||
All Templates
|
All Templates
|
||||||
</Text>
|
</Text>
|
||||||
{regularTemplates.length > 0 ? (
|
{regularTemplates.length > 0 ? (
|
||||||
@ -241,8 +266,6 @@ export default function TemplatesScreen() {
|
|||||||
<View className="h-20" />
|
<View className="h-20" />
|
||||||
</ScrollView>
|
</ScrollView>
|
||||||
|
|
||||||
{/* Remove the TemplateDetails component since we're using router navigation now */}
|
|
||||||
|
|
||||||
<FloatingActionButton
|
<FloatingActionButton
|
||||||
icon={Plus}
|
icon={Plus}
|
||||||
onPress={() => setShowNewTemplate(true)}
|
onPress={() => setShowNewTemplate(true)}
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
// app/(tabs)/profile.tsx
|
// app/(tabs)/profile.tsx
|
||||||
import { View, ScrollView, ImageBackground } from 'react-native';
|
import { View, ScrollView, ImageBackground } from 'react-native';
|
||||||
import { useState, useEffect } from 'react';
|
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 { H1 } from '@/components/ui/typography';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { Text } from '@/components/ui/text';
|
import { Text } from '@/components/ui/text';
|
||||||
@ -67,7 +67,21 @@ export default function ProfileScreen() {
|
|||||||
if (!isAuthenticated) {
|
if (!isAuthenticated) {
|
||||||
return (
|
return (
|
||||||
<TabScreen>
|
<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="flex-1 items-center justify-center p-6">
|
||||||
<View className="items-center mb-8">
|
<View className="items-center mb-8">
|
||||||
<UserAvatar
|
<UserAvatar
|
||||||
@ -102,14 +116,17 @@ export default function ProfileScreen() {
|
|||||||
return (
|
return (
|
||||||
<TabScreen>
|
<TabScreen>
|
||||||
<Header
|
<Header
|
||||||
title="Profile"
|
useLogo={true}
|
||||||
rightElement={
|
rightElement={
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="icon"
|
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>
|
</Button>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
@ -23,6 +23,9 @@ import OverviewTab from './index';
|
|||||||
import SocialTab from './social';
|
import SocialTab from './social';
|
||||||
import HistoryTab from './history';
|
import HistoryTab from './history';
|
||||||
|
|
||||||
|
// Import the shared context
|
||||||
|
import { TemplateContext } from './_templateContext';
|
||||||
|
|
||||||
// Types
|
// Types
|
||||||
import { WorkoutTemplate } from '@/types/templates';
|
import { WorkoutTemplate } from '@/types/templates';
|
||||||
import type { CustomTheme } from '@/lib/theme';
|
import type { CustomTheme } from '@/lib/theme';
|
||||||
@ -263,21 +266,3 @@ export default function TemplateDetailsLayout() {
|
|||||||
</TabScreen>
|
</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;
|
|
||||||
}
|
|
27
app/(workout)/template/[id]/_templateContext.tsx
Normal file
27
app/(workout)/template/[id]/_templateContext.tsx
Normal 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;
|
||||||
|
}
|
@ -6,7 +6,7 @@ import { Button } from '@/components/ui/button';
|
|||||||
import { Badge } from '@/components/ui/badge';
|
import { Badge } from '@/components/ui/badge';
|
||||||
import { Card, CardContent } from '@/components/ui/card';
|
import { Card, CardContent } from '@/components/ui/card';
|
||||||
import { Calendar } from 'lucide-react-native';
|
import { Calendar } from 'lucide-react-native';
|
||||||
import { useTemplate } from './_layout';
|
import { useTemplate } from './_templateContext';
|
||||||
|
|
||||||
// Format date helper
|
// Format date helper
|
||||||
const formatDate = (date: Date) => {
|
const formatDate = (date: Date) => {
|
||||||
|
@ -6,7 +6,7 @@ import { Button } from '@/components/ui/button';
|
|||||||
import { Badge } from '@/components/ui/badge';
|
import { Badge } from '@/components/ui/badge';
|
||||||
import { Separator } from '@/components/ui/separator';
|
import { Separator } from '@/components/ui/separator';
|
||||||
import { Card, CardContent } from '@/components/ui/card';
|
import { Card, CardContent } from '@/components/ui/card';
|
||||||
import { useTemplate } from './_layout';
|
import { useTemplate } from './_templateContext';
|
||||||
import { formatTime } from '@/utils/formatTime';
|
import { formatTime } from '@/utils/formatTime';
|
||||||
import {
|
import {
|
||||||
Edit2,
|
Edit2,
|
||||||
|
@ -11,7 +11,7 @@ import {
|
|||||||
Repeat,
|
Repeat,
|
||||||
Bookmark
|
Bookmark
|
||||||
} from 'lucide-react-native';
|
} from 'lucide-react-native';
|
||||||
import { useTemplate } from './_layout';
|
import { useTemplate } from './_templateContext';
|
||||||
import { cn } from '@/lib/utils';
|
import { cn } from '@/lib/utils';
|
||||||
|
|
||||||
// Mock social feed data
|
// Mock social feed data
|
||||||
|
@ -7,6 +7,7 @@ import { Bell } from 'lucide-react-native';
|
|||||||
import { Text } from '@/components/ui/text';
|
import { Text } from '@/components/ui/text';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import UserAvatar from '@/components/UserAvatar';
|
import UserAvatar from '@/components/UserAvatar';
|
||||||
|
import PowerLogo from '@/components/PowerLogo';
|
||||||
import { useSettingsDrawer } from '@/lib/contexts/SettingsDrawerContext';
|
import { useSettingsDrawer } from '@/lib/contexts/SettingsDrawerContext';
|
||||||
import { useNDKCurrentUser } from '@/lib/hooks/useNDK';
|
import { useNDKCurrentUser } from '@/lib/hooks/useNDK';
|
||||||
|
|
||||||
@ -14,12 +15,14 @@ interface HeaderProps {
|
|||||||
title?: string;
|
title?: string;
|
||||||
hideTitle?: boolean;
|
hideTitle?: boolean;
|
||||||
rightElement?: React.ReactNode;
|
rightElement?: React.ReactNode;
|
||||||
|
useLogo?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function Header({
|
export default function Header({
|
||||||
title,
|
title,
|
||||||
hideTitle = false,
|
hideTitle = false,
|
||||||
rightElement
|
rightElement,
|
||||||
|
useLogo = false
|
||||||
}: HeaderProps) {
|
}: HeaderProps) {
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
const insets = useSafeAreaInsets();
|
const insets = useSafeAreaInsets();
|
||||||
@ -56,9 +59,13 @@ export default function Header({
|
|||||||
fallback={fallbackLetter}
|
fallback={fallbackLetter}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Middle - Title */}
|
{/* Middle - Title or Logo */}
|
||||||
<View style={styles.titleContainer}>
|
<View style={[styles.titleContainer, { marginLeft: 10 }]}>
|
||||||
<Text style={styles.title}>{title}</Text>
|
{useLogo ? (
|
||||||
|
<PowerLogo size="md" />
|
||||||
|
) : (
|
||||||
|
<Text style={styles.title}>{title}</Text>
|
||||||
|
)}
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
{/* Right side - Custom element or default notifications */}
|
{/* Right side - Custom element or default notifications */}
|
||||||
@ -69,7 +76,11 @@ export default function Header({
|
|||||||
size="icon"
|
size="icon"
|
||||||
onPress={() => {}}
|
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>
|
</Button>
|
||||||
)}
|
)}
|
||||||
</View>
|
</View>
|
||||||
@ -93,7 +104,9 @@ const styles = StyleSheet.create({
|
|||||||
titleContainer: {
|
titleContainer: {
|
||||||
flex: 1,
|
flex: 1,
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
paddingHorizontal: 16,
|
justifyContent: 'center',
|
||||||
|
paddingHorizontal: 0, // Remove padding to allow more precise positioning
|
||||||
|
height: '100%',
|
||||||
},
|
},
|
||||||
title: {
|
title: {
|
||||||
fontSize: 20,
|
fontSize: 20,
|
||||||
|
57
components/PowerLogo.tsx
Normal file
57
components/PowerLogo.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
@ -176,48 +176,6 @@ export const SimplifiedExerciseList = ({
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</View>
|
</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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -26,6 +26,7 @@ export interface TemplateExerciseDisplay {
|
|||||||
title: string;
|
title: string;
|
||||||
targetSets: number;
|
targetSets: number;
|
||||||
targetReps: number;
|
targetReps: number;
|
||||||
|
equipment?: string;
|
||||||
notes?: string;
|
notes?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user