minor ui update

This commit is contained in:
DocNR 2025-02-15 14:03:42 -05:00
parent af6a5c2683
commit 147ea582fc
14 changed files with 246 additions and 442 deletions

Binary file not shown.

View File

@ -3,26 +3,25 @@ import React from 'react';
import { Platform } from 'react-native';
import { Tabs } from 'expo-router';
import { useTheme } from '@react-navigation/native';
import { Dumbbell, Library, Users, History, User } from 'lucide-react-native';
import { convertHSLValues } from '@/lib/theme';
import { Dumbbell, Library, Users, History, User, } from 'lucide-react-native';
import type { CustomTheme } from '@/lib/theme';
export default function TabLayout() {
const { colors, dark } = useTheme();
const { purple, mutedForeground } = convertHSLValues(dark ? 'dark' : 'light');
const theme = useTheme() as CustomTheme;
return (
<Tabs
screenOptions={{
headerShown: false,
tabBarStyle: {
backgroundColor: colors.background,
borderTopColor: colors.border,
backgroundColor: theme.colors.background,
borderTopColor: theme.colors.border,
borderTopWidth: Platform.OS === 'ios' ? 0.5 : 1,
elevation: 0,
shadowOpacity: 0,
},
tabBarActiveTintColor: purple,
tabBarInactiveTintColor: mutedForeground,
tabBarActiveTintColor: theme.colors.tabActive,
tabBarInactiveTintColor: theme.colors.tabInactive,
tabBarShowLabel: true,
tabBarLabelStyle: {
fontSize: 12,

View File

@ -1,11 +1,29 @@
// app/(tabs)/history.tsx
// app/(tabs)/history.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';
export default function HomeScreen() {
export default function HistoryScreen() {
return (
<View className="flex-1 items-center justify-center">
<Text>Home Screen</Text>
</View>
<TabScreen>
<Header
title="History"
rightElement={
<Button
variant="ghost"
size="icon"
onPress={() => console.log('Open filters')}
>
<Filter className="text-foreground" />
</Button>
}
/>
<View className="flex-1 items-center justify-center">
<Text>History Screen</Text>
</View>
</TabScreen>
);
}

View File

@ -1,11 +1,29 @@
// app/(tabs)/index.tsx
// app/(tabs)/index.tsx (Workout tab)
import { View } from 'react-native';
import { Text } from '@/components/ui/text';
import { TabScreen } from '@/components/layout/TabScreen';
import Header from '@/components/Header';
import { Plus } from 'lucide-react-native';
import { Button } from '@/components/ui/button';
export default function HomeScreen() {
export default function WorkoutScreen() {
return (
<View className="flex-1 items-center justify-center">
<Text>Home Screen</Text>
</View>
<TabScreen>
<Header
title="Workout"
rightElement={
<Button
variant="ghost"
size="icon"
onPress={() => console.log('New workout')}
>
<Plus className="text-foreground" />
</Button>
}
/>
<View className="flex-1 items-center justify-center">
<Text>Workout Screen</Text>
</View>
</TabScreen>
);
}

View File

@ -1,124 +0,0 @@
// app/(tabs)/library/_layout.native.tsx
import { View } from 'react-native';
import { createMaterialTopTabNavigator } from '@react-navigation/material-top-tabs';
import { Text } from '@/components/ui/text';
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 { convertHSLValues } from '@/lib/theme';
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[] // Fixed: Create mutable array of SourceType
};
// Initial filter state
const initialFilters: FilterOptions = {
equipment: [],
tags: [],
source: []
};
export default function LibraryLayout() {
const { colors, dark } = useTheme();
const { purple, mutedForeground } = convertHSLValues(dark ? 'dark' : 'light');
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);
// Count total active filters
const totalFilters = Object.values(filters).reduce(
(acc, curr) => acc + curr.length,
0
);
setActiveFilters(totalFilters);
};
return (
<View className="flex-1">
<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>
}
>
<Text className="text-muted-foreground text-sm mt-1">
12 exercises 5 templates
</Text>
</Header>
<FilterSheet
isOpen={filterSheetOpen}
onClose={() => setFilterSheetOpen(false)}
options={currentFilters}
onApplyFilters={handleApplyFilters}
availableFilters={availableFilters}
/>
<Tab.Navigator
initialRouteName="templates"
screenOptions={{
tabBarActiveTintColor: purple,
tabBarInactiveTintColor: mutedForeground,
tabBarLabelStyle: {
fontSize: 14,
textTransform: 'capitalize',
fontWeight: 'bold',
},
tabBarIndicatorStyle: {
backgroundColor: purple,
height: 2,
},
tabBarStyle: {
backgroundColor: colors.background,
elevation: 0,
shadowOpacity: 0,
borderBottomWidth: 1,
borderBottomColor: colors.border,
},
tabBarPressColor: colors.primary,
}}
>
<Tab.Screen
name="exercises"
component={ExercisesScreen}
options={{ title: 'Exercises' }}
/>
<Tab.Screen
name="templates"
component={TemplatesScreen}
options={{ title: 'Templates' }}
/>
<Tab.Screen
name="programs"
component={ProgramsScreen}
options={{ title: 'Programs' }}
/>
</Tab.Navigator>
</View>
);
}

View File

@ -1,3 +1,119 @@
// app/(tabs)/library/_layout.tsx
// This is the fallback layout that gets overridden by platform-specific files
export { default } from './_layout.native';
import { View } from 'react-native';
import { createMaterialTopTabNavigator } from '@react-navigation/material-top-tabs';
import { Text } from '@/components/ui/text';
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}
/>
<Tab.Navigator
initialRouteName="templates"
screenOptions={{
tabBarActiveTintColor: theme.colors.tabIndicator,
tabBarInactiveTintColor: theme.colors.tabInactive,
tabBarLabelStyle: {
fontSize: 14,
textTransform: 'capitalize',
fontWeight: 'bold',
},
tabBarIndicatorStyle: {
backgroundColor: theme.colors.tabIndicator,
height: 2,
},
tabBarStyle: {
backgroundColor: theme.colors.background,
elevation: 0,
shadowOpacity: 0,
borderBottomWidth: 1,
borderBottomColor: theme.colors.border,
},
tabBarPressColor: theme.colors.primary,
}}
>
<Tab.Screen
name="exercises"
component={ExercisesScreen}
options={{ title: 'Exercises' }}
/>
<Tab.Screen
name="templates"
component={TemplatesScreen}
options={{ title: 'Templates' }}
/>
<Tab.Screen
name="programs"
component={ProgramsScreen}
options={{ title: 'Programs' }}
/>
</Tab.Navigator>
</TabScreen>
);
}

View File

@ -1,80 +0,0 @@
// app/(tabs)/library/_layout.web.tsx
import React from 'react';
import { View, Pressable } from 'react-native';
import { Text } from '@/components/ui/text';
import { ThemeToggle } from '@/components/ThemeToggle';
import Pager from '@/components/pager';
import { CUSTOM_COLORS } from '@/lib/constants';
import type { PagerRef } from '@/components/pager/types';
import ExercisesScreen from './exercises';
import TemplatesScreen from './templates';
import ProgramsScreen from './programs';
const tabs = [
{ key: 'exercises', title: 'Exercises', component: ExercisesScreen },
{ key: 'templates', title: 'Templates', component: TemplatesScreen },
{ key: 'programs', title: 'Programs', component: ProgramsScreen },
];
export default function LibraryLayout() {
const [activeIndex, setActiveIndex] = React.useState(0);
const pagerRef = React.useRef<PagerRef>(null);
const handleTabPress = (index: number) => {
setActiveIndex(index);
pagerRef.current?.setPage(index);
};
return (
<View className="flex-1 bg-background">
{/* Header */}
<View className="flex-row justify-between items-center px-4 pt-14 pb-4 bg-card">
<Text className="text-2xl font-bold">Library</Text>
<ThemeToggle />
</View>
{/* Tab Headers */}
<View className="flex-row bg-card border-b border-border">
{tabs.map((tab, index) => (
<View key={tab.key} className="flex-1">
<Pressable
onPress={() => handleTabPress(index)}
className="px-4 py-3 items-center"
>
<Text
className={activeIndex === index
? 'font-semibold text-primary'
: 'font-semibold text-muted-foreground'}
style={activeIndex === index ? { color: CUSTOM_COLORS.purple } : undefined}
>
{tab.title}
</Text>
</Pressable>
{activeIndex === index && (
<View
className="h-0.5"
style={{ backgroundColor: CUSTOM_COLORS.purple }}
/>
)}
</View>
))}
</View>
{/* Content */}
<View style={{ flex: 1 }}>
<Pager
ref={pagerRef}
initialPage={0}
onPageSelected={(e) => setActiveIndex(e.nativeEvent.position)}
style={{ flex: 1 }}
>
{tabs.map((tab) => (
<View key={tab.key} style={{ flex: 1 }}>
<tab.component />
</View>
))}
</Pager>
</View>
</View>
);
}

View File

@ -1,95 +0,0 @@
// app/(tabs)/library/index.tsx
import React from 'react';
import { View, StyleSheet, Platform } from 'react-native';
import { createMaterialTopTabNavigator } from '@react-navigation/material-top-tabs';
import { useTheme } from '@react-navigation/native';
import { Text } from '@/components/ui/text';
import { ThemeToggle } from '@/components/ThemeToggle';
import ExercisesScreen from './exercises';
const Tab = createMaterialTopTabNavigator();
function TemplatesTab() {
return (
<View className="flex-1 items-center justify-center">
<Text>Templates Content</Text>
</View>
);
}
function ProgramsTab() {
return (
<View className="flex-1 items-center justify-center">
<Text>Programs (Coming Soon)</Text>
</View>
);
}
export default function LibraryScreen() {
const { colors } = useTheme();
return (
<View style={styles.container}>
{/* Header with Theme Toggle */}
<View style={[styles.header, { backgroundColor: colors.card }]}>
<Text className="text-2xl font-bold">Library</Text>
<ThemeToggle />
</View>
{/* Material Top Tabs */}
<Tab.Navigator
screenOptions={{
tabBarActiveTintColor: colors.text,
tabBarInactiveTintColor: 'grey',
tabBarLabelStyle: {
fontSize: 14,
textTransform: 'capitalize',
fontWeight: 'bold',
},
tabBarIndicatorStyle: {
backgroundColor: colors.text,
},
tabBarStyle: { backgroundColor: colors.card },
swipeEnabled: true, // Enable swipe navigation
animationEnabled: true, // Enable animations when swiping
lazy: true }}
>
<Tab.Screen
name="exercises"
component={ExercisesScreen}
options={{
title: 'Exercises',
}}
/>
<Tab.Screen
name="templates"
component={TemplatesTab}
options={{
title: 'Templates',
}}
/>
<Tab.Screen
name="programs"
component={ProgramsTab}
options={{
title: 'Programs',
}}
/>
</Tab.Navigator>
</View>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
},
header: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
paddingHorizontal: 16,
paddingTop: Platform.OS === 'ios' ? 60 : 16,
paddingBottom: 16,
},
});

View File

@ -1,5 +1,4 @@
// app/(tabs)/profile.tsx
import React from 'react';
import { View, ScrollView } from 'react-native';
import { Settings } from 'lucide-react-native';
import { H1 } from '@/components/ui/typography';
@ -8,6 +7,7 @@ import { Button } from '@/components/ui/button';
import { Text } from '@/components/ui/text';
import Header from '@/components/Header';
import { useSafeAreaInsets } from 'react-native-safe-area-context';
import { TabScreen } from '@/components/layout/TabScreen';
const PLACEHOLDER_IMAGE = 'https://github.com/shadcn.png';
@ -15,16 +15,14 @@ export default function ProfileScreen() {
const insets = useSafeAreaInsets();
return (
<View className="flex-1">
<TabScreen>
<Header
title="Profile"
rightElement={
<Button
variant="ghost"
size="icon"
onPress={() => {
console.log('Open settings');
}}
onPress={() => console.log('Open settings')}
>
<Settings className="text-foreground" />
</Button>
@ -37,7 +35,7 @@ export default function ProfileScreen() {
paddingBottom: insets.bottom + 20
}}
>
{/* Profile Header Section */}
{/* Profile content remains the same */}
<View className="items-center pt-6 pb-8">
<Avatar className="w-24 h-24 mb-4" alt="Profile picture">
<AvatarImage source={{ uri: PLACEHOLDER_IMAGE }} />
@ -49,7 +47,6 @@ export default function ProfileScreen() {
<Text className="text-muted-foreground">@johndoe</Text>
</View>
{/* Stats Section */}
<View className="flex-row justify-around px-4 py-6 bg-card">
<View className="items-center">
<Text className="text-2xl font-bold">24</Text>
@ -65,39 +62,18 @@ export default function ProfileScreen() {
</View>
</View>
{/* Profile Actions */}
<View className="p-4 gap-2">
<Button
variant="outline"
className="mb-2"
onPress={() => {
console.log('Edit profile');
}}
>
<Button variant="outline" className="mb-2">
<Text>Edit Profile</Text>
</Button>
<Button
variant="outline"
className="mb-2"
onPress={() => {
console.log('Account settings');
}}
>
<Button variant="outline" className="mb-2">
<Text>Account Settings</Text>
</Button>
<Button
variant="outline"
className="mb-2"
onPress={() => {
console.log('Preferences');
}}
>
<Button variant="outline" className="mb-2">
<Text>Preferences</Text>
</Button>
</View>
</ScrollView>
</View>
</TabScreen>
);
}

View File

@ -1,25 +1,22 @@
// app/(tabs)/index.tsx (and similar for other tab screens)
// app/(tabs)/social.tsx
import { View } from 'react-native';
import { Text } from '@/components/ui/text';
import { Button } from '@/components/ui/button';
import { Bell } from 'lucide-react-native';
import Header from '@/components/Header';
import { TabScreen } from '@/components/layout/TabScreen';
export default function SocialScreen() {
return (
<View className="flex-1">
<TabScreen>
<Header
title="Social"
rightElement={
<Button
variant="ghost"
size="icon"
onPress={() => {
// TODO: Open notifications
console.log('Open notifications');
}}
onPress={() => console.log('Open notifications')}
>
{/* Add a notification badge if needed */}
<View className="relative">
<Bell className="text-foreground" />
<View className="absolute -top-1 -right-1 w-2 h-2 bg-primary rounded-full" />
@ -27,6 +24,9 @@ export default function SocialScreen() {
</Button>
}
/>
</View>
<View className="flex-1 items-center justify-center">
<Text>Social Screen</Text>
</View>
</TabScreen>
);
}
}

View File

@ -1,6 +1,6 @@
// app/_layout.tsx
import '@/global.css';
import { DarkTheme, DefaultTheme, Theme, ThemeProvider } from '@react-navigation/native';
import { DarkTheme, DefaultTheme, ThemeProvider } from '@react-navigation/native';
import { Stack } from 'expo-router';
import { StatusBar } from 'expo-status-bar';
import * as React from 'react';
@ -10,12 +10,14 @@ import { useColorScheme } from '@/lib/useColorScheme';
import { PortalHost } from '@rn-primitives/portal';
import { setAndroidNavigationBar } from '@/lib/android-navigation-bar';
import { GestureHandlerRootView } from 'react-native-gesture-handler';
import type { ExtendedTheme } from '@react-navigation/native';
const LIGHT_THEME: Theme = {
const LIGHT_THEME: ExtendedTheme = {
...DefaultTheme,
colors: NAV_THEME.light,
};
const DARK_THEME: Theme = {
const DARK_THEME: ExtendedTheme = {
...DarkTheme,
colors: NAV_THEME.dark,
};

View File

@ -0,0 +1,23 @@
// components/layout/TabScreen.tsx
import React from 'react';
import { View, ViewProps } from 'react-native';
interface TabScreenProps extends ViewProps {
children: React.ReactNode;
}
export function TabScreen({
children,
style,
...props
}: TabScreenProps) {
return (
<View
className="flex-1 bg-background"
style={style}
{...props}
>
{children}
</View>
);
}

View File

@ -12,8 +12,8 @@ export const NAV_THEME: {
notification: 'hsl(0, 84.2%, 60.2%)',
primary: 'hsl(261, 90%, 66%)',
text: 'hsl(240, 10%, 3.9%)',
tabActive: 'hsl(261, 90%, 66%)',
tabInactive: 'hsl(240, 3.8%, 46.1%)',
tabActive: '#000000',
tabInactive: '#737373',
tabIndicator: 'hsl(261, 90%, 66%)',
},
dark: {
@ -23,8 +23,8 @@ export const NAV_THEME: {
notification: 'hsl(0, 72%, 51%)',
primary: 'hsl(261, 90%, 66%)',
text: 'hsl(0, 0%, 98%)',
tabActive: 'hsl(261, 90%, 66%)',
tabInactive: 'hsl(240, 5%, 64.9%)',
tabActive: '#FFFFFF',
tabInactive: '#A3A3A3',
tabIndicator: 'hsl(261, 90%, 66%)',
},
};

View File

@ -1,8 +1,8 @@
// lib/theme.ts
import { Theme } from '@react-navigation/native';
import type { DarkTheme, DefaultTheme } from '@react-navigation/native';
import { ColorSchemeName } from 'react-native';
// Update the interface for our navigation theme colors
// Define our colors type based on the built-in theme types
export interface NavigationThemeColors {
primary: string;
background: string;
@ -15,79 +15,30 @@ export interface NavigationThemeColors {
tabIndicator: string;
}
// Original ThemeColors interface remains the same
export interface ThemeColors {
background: string;
foreground: string;
card: string;
'card-foreground': string;
popover: string;
'popover-foreground': string;
primary: string;
'primary-foreground': string;
secondary: string;
'secondary-foreground': string;
muted: string;
'muted-foreground': string;
accent: string;
'accent-foreground': string;
destructive: string;
'destructive-foreground': string;
border: string;
input: string;
ring: string;
purple: string;
'purple-pressed': string;
// Define our custom theme type using the existing theme types
export interface CustomTheme {
dark: boolean;
colors: NavigationThemeColors;
}
// Export the color conversion function
export function convertHSLValues(colorScheme: 'light' | 'dark') {
const purple = colorScheme === 'light'
? 'hsl(261, 90%, 66%)'
: 'hsl(261, 90%, 66%)';
const mutedForeground = colorScheme === 'light'
? 'hsl(240, 3.8%, 46.1%)'
: 'hsl(240, 5%, 64.9%)';
export function getNavigationTheme(scheme: ColorSchemeName): CustomTheme {
const colorScheme = scheme ?? 'light';
const isDark = colorScheme === 'dark';
return {
purple,
mutedForeground,
};
}
export function getNavigationTheme(scheme: ColorSchemeName): Theme {
const colorScheme = scheme ?? 'light';
const { purple, mutedForeground } = convertHSLValues(colorScheme as 'light' | 'dark');
const theme: Theme = {
dark: colorScheme === 'dark',
colors: {
primary: purple,
background: colorScheme === 'dark' ? 'hsl(240, 10%, 3.9%)' : 'hsl(0, 0%, 100%)',
card: colorScheme === 'dark' ? 'hsl(240, 10%, 5.9%)' : 'hsl(0, 0%, 100%)',
text: colorScheme === 'dark' ? 'hsl(0, 0%, 98%)' : 'hsl(240, 10%, 3.9%)',
border: colorScheme === 'dark' ? 'hsl(240, 3.7%, 15.9%)' : 'hsl(240, 5.9%, 90%)',
notification: colorScheme === 'dark' ? 'hsl(0, 72%, 51%)' : 'hsl(0, 84.2%, 60.2%)',
},
fonts: {
regular: {
fontFamily: 'System',
fontWeight: '400',
},
medium: {
fontFamily: 'System',
fontWeight: '500',
},
bold: {
fontFamily: 'System',
fontWeight: '700',
},
heavy: {
fontFamily: 'System',
fontWeight: '900',
},
},
};
return theme;
}
const theme: CustomTheme = {
dark: isDark,
colors: {
primary: 'hsl(261, 90%, 66%)',
background: isDark ? 'hsl(240, 10%, 3.9%)' : 'hsl(0, 0%, 100%)',
card: isDark ? 'hsl(240, 10%, 5.9%)' : 'hsl(0, 0%, 100%)',
text: isDark ? 'hsl(0, 0%, 98%)' : 'hsl(240, 10%, 3.9%)',
border: isDark ? 'hsl(240, 3.7%, 15.9%)' : 'hsl(240, 5.9%, 90%)',
notification: isDark ? 'hsl(0, 72%, 51%)' : 'hsl(0, 84.2%, 60.2%)',
tabActive: isDark ? '#FFFFFF' : '#000000',
tabInactive: isDark ? '#A3A3A3' : '#737373',
tabIndicator: 'hsl(261, 90%, 66%)',
}
};
return theme;
}