redesign completed

alot of work ahead
This commit is contained in:
dimitar committed 2025-11-26 02:31:46 +01:00
1 parent 73d2c4c1ed
commit febcdc111e
18 files changed
+1591 -845

No files matched your search

@@ -0,0 +1,157 @@
import React from 'react';
import { View, Text, StyleSheet, Dimensions } from 'react-native';
import { LinearGradient } from 'expo-linear-gradient';
import { Ionicons } from '@expo/vector-icons';
import { theme } from '../styles/theme';
const { width } = Dimensions.get('window');
interface ActivityWidgetProps {
steps: number;
calories: number;
duration: number; // in minutes
}
export function ActivityWidget({ steps, calories, duration }: ActivityWidgetProps) {
return (
<View style={styles.container}>
<LinearGradient
colors={theme.gradients.dark}
start={{ x: 0, y: 0 }}
end={{ x: 1, y: 1 }}
style={[styles.card, theme.shadows.medium]}
>
<View style={styles.header}>
<Text style={styles.title}>Daily Activity</Text>
<Ionicons name="stats-chart" size={20} color={theme.colors.primary} />
</View>
<View style={styles.statsRow}>
<View style={styles.statItem}>
<View style={[styles.iconContainer, { backgroundColor: 'rgba(59, 130, 246, 0.2)' }]}>
<Ionicons name="footsteps" size={20} color="#3b82f6" />
</View>
<Text style={styles.statValue}>{steps.toLocaleString()}</Text>
<Text style={styles.statLabel}>Steps</Text>
</View>
<View style={styles.divider} />
<View style={styles.statItem}>
<View style={[styles.iconContainer, { backgroundColor: 'rgba(239, 68, 68, 0.2)' }]}>
<Ionicons name="flame" size={20} color="#ef4444" />
</View>
<Text style={styles.statValue}>{calories}</Text>
<Text style={styles.statLabel}>Kcal</Text>
</View>
<View style={styles.divider} />
<View style={styles.statItem}>
<View style={[styles.iconContainer, { backgroundColor: 'rgba(16, 185, 129, 0.2)' }]}>
<Ionicons name="time" size={20} color="#10b981" />
</View>
<Text style={styles.statValue}>{duration}m</Text>
<Text style={styles.statLabel}>Active</Text>
</View>
</View>
{/* Simple Bar Chart Visualization */}
<View style={styles.chartContainer}>
{[0.4, 0.6, 0.3, 0.8, 0.5, 0.9, 0.7].map((height, index) => (
<View key={index} style={styles.barContainer}>
<LinearGradient
colors={theme.gradients.primaryVertical}
style={[styles.bar, { height: height * 60 }]}
/>
<Text style={styles.dayLabel}>
{['M', 'T', 'W', 'T', 'F', 'S', 'S'][index]}
</Text>
</View>
))}
</View>
</LinearGradient>
</View>
);
}
const styles = StyleSheet.create({
container: {
marginHorizontal: 20,
marginBottom: 20,
},
card: {
borderRadius: 24,
padding: 20,
borderWidth: 1,
borderColor: 'rgba(255, 255, 255, 0.1)',
},
header: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
marginBottom: 20,
},
title: {
fontSize: 18,
fontWeight: '700',
color: '#fff',
},
statsRow: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
marginBottom: 24,
},
statItem: {
alignItems: 'center',
flex: 1,
},
iconContainer: {
width: 40,
height: 40,
borderRadius: 20,
justifyContent: 'center',
alignItems: 'center',
marginBottom: 8,
},
statValue: {
fontSize: 20,
fontWeight: '700',
color: '#fff',
marginBottom: 2,
},
statLabel: {
fontSize: 12,
color: theme.colors.gray400,
fontWeight: '500',
},
divider: {
width: 1,
height: 40,
backgroundColor: 'rgba(255, 255, 255, 0.1)',
},
chartContainer: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'flex-end',
height: 80,
paddingTop: 10,
borderTopWidth: 1,
borderTopColor: 'rgba(255, 255, 255, 0.1)',
},
barContainer: {
alignItems: 'center',
gap: 8,
},
bar: {
width: 6,
borderRadius: 3,
opacity: 0.8,
},
dayLabel: {
fontSize: 10,
color: theme.colors.gray500,
fontWeight: '600',
},
});
+67 -49
View File
@@ -15,11 +15,13 @@ import {
} from 'react-native';
import { LinearGradient } from 'expo-linear-gradient';
import { theme } from '../styles/theme';
import { haptics } from '../utils/haptics';
interface AnimatedButtonProps {
onPress: () => void;
title: string;
variant?: 'primary' | 'secondary' | 'danger' | 'success';
onPress: () => void;
variant?: 'primary' | 'secondary' | 'success' | 'danger' | 'warning';
size?: 'sm' | 'md' | 'lg';
loading?: boolean;
disabled?: boolean;
style?: ViewStyle;
@@ -28,109 +30,125 @@ interface AnimatedButtonProps {
}
export function AnimatedButton({
onPress,
title,
onPress,
variant = 'primary',
size = 'md',
loading = false,
disabled = false,
style,
textStyle,
icon,
}: AnimatedButtonProps) {
const scaleAnim = useRef(new Animated.Value(1)).current;
const scale = useRef(new Animated.Value(1)).current;
const handlePressIn = () => {
Animated.spring(scaleAnim, {
Animated.spring(scale, {
toValue: 0.95,
friction: 8,
tension: 100,
useNativeDriver: true,
}).start();
};
const handlePressOut = () => {
Animated.spring(scaleAnim, {
Animated.spring(scale, {
toValue: 1,
friction: 8,
tension: 100,
useNativeDriver: true,
}).start();
};
const getGradientColors = (): readonly [string, string, ...string[]] => {
const handlePress = () => {
if (!disabled && !loading) {
haptics.light();
onPress();
}
};
const getGradientColors = () => {
if (disabled) return ['#9ca3af', '#6b7280'] as const;
switch (variant) {
case 'primary':
return theme.gradients.primary;
case 'danger':
return theme.gradients.danger;
case 'secondary':
return theme.gradients.purple;
case 'success':
return theme.gradients.success;
case 'secondary':
return [theme.colors.gray600, theme.colors.gray700] as const;
case 'danger':
return theme.gradients.danger;
case 'warning':
return theme.gradients.warning;
default:
return theme.gradients.primary;
}
};
const getShadowStyle = () => {
if (disabled) return {};
switch (variant) {
case 'danger':
return theme.shadows.glowDanger;
const getSizeStyles = () => {
switch (size) {
case 'sm':
return { paddingVertical: 8, paddingHorizontal: 16, fontSize: 14 };
case 'lg':
return { paddingVertical: 16, paddingHorizontal: 32, fontSize: 18 };
default:
return theme.shadows.glow;
return { paddingVertical: 12, paddingHorizontal: 24, fontSize: 16 };
}
};
const sizeStyles = getSizeStyles();
return (
<Animated.View style={[{ transform: [{ scale: scaleAnim }] }, style]}>
<TouchableOpacity
onPress={onPress}
onPressIn={handlePressIn}
onPressOut={handlePressOut}
disabled={disabled || loading}
activeOpacity={0.9}
>
<TouchableOpacity
onPressIn={handlePressIn}
onPressOut={handlePressOut}
onPress={handlePress}
disabled={disabled || loading}
activeOpacity={0.9}
style={[styles.container, style]}
>
<Animated.View style={{ transform: [{ scale }], width: '100%' }}>
<LinearGradient
colors={getGradientColors()}
start={{ x: 0, y: 0 }}
end={{ x: 1, y: 0 }}
style={[
styles.button,
getShadowStyle(),
disabled && styles.disabled,
styles.gradient,
{ paddingVertical: sizeStyles.paddingVertical, paddingHorizontal: sizeStyles.paddingHorizontal },
]}
>
{loading ? (
<ActivityIndicator color="#fff" />
) : (
<>
{icon}
<Text style={[styles.text, textStyle]}>{title}</Text>
{icon && <React.Fragment>{icon}</React.Fragment>}
<Text
style={[
styles.text,
{ fontSize: sizeStyles.fontSize },
icon ? { marginLeft: 8 } : {},
textStyle,
]}
>
{title}
</Text>
</>
)}
</LinearGradient>
</TouchableOpacity>
</Animated.View>
</Animated.View>
</TouchableOpacity>
);
}
const styles = StyleSheet.create({
button: {
paddingVertical: 16,
paddingHorizontal: 24,
borderRadius: theme.borderRadius.lg,
container: {
alignItems: 'center',
justifyContent: 'center',
},
gradient: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'center',
gap: 8,
borderRadius: 12,
...theme.shadows.medium,
},
text: {
color: theme.colors.white,
fontSize: theme.typography.fontSize.base,
fontWeight: theme.typography.fontWeight.semibold,
},
disabled: {
opacity: 0.5,
color: '#fff',
fontWeight: '600',
textAlign: 'center',
},
});
@@ -0,0 +1,72 @@
import React, { useEffect, useRef } from 'react';
import { View, Animated, StyleSheet, Text } from 'react-native';
import Svg, { Circle } from 'react-native-svg';
import { theme } from '../styles/theme';
const AnimatedCircle = Animated.createAnimatedComponent(Circle);
interface CircularProgressProps {
size?: number;
strokeWidth?: number;
progress: number; // 0 to 100
color?: string;
backgroundColor?: string;
}
export function CircularProgress({
size = 60,
strokeWidth = 6,
progress,
color = theme.colors.primary,
backgroundColor = theme.colors.gray200,
}: CircularProgressProps) {
const animatedValue = useRef(new Animated.Value(0)).current;
const radius = (size - strokeWidth) / 2;
const circumference = radius * 2 * Math.PI;
useEffect(() => {
Animated.timing(animatedValue, {
toValue: progress,
duration: 1000,
useNativeDriver: true,
}).start();
}, [progress]);
const strokeDashoffset = animatedValue.interpolate({
inputRange: [0, 100],
outputRange: [circumference, 0],
});
return (
<View style={{ width: size, height: size, justifyContent: 'center', alignItems: 'center' }}>
<Svg width={size} height={size} viewBox={`0 0 ${size} ${size}`}>
<Circle
cx={size / 2}
cy={size / 2}
r={radius}
stroke={backgroundColor}
strokeWidth={strokeWidth}
fill="transparent"
/>
<AnimatedCircle
cx={size / 2}
cy={size / 2}
r={radius}
stroke={color}
strokeWidth={strokeWidth}
fill="transparent"
strokeDasharray={circumference}
strokeDashoffset={strokeDashoffset}
strokeLinecap="round"
rotation="-90"
origin={`${size / 2}, ${size / 2}`}
/>
</Svg>
<View style={[StyleSheet.absoluteFillObject, { justifyContent: 'center', alignItems: 'center' }]}>
<Text style={{ fontSize: size * 0.25, fontWeight: 'bold', color: theme.colors.gray900 }}>
{Math.round(progress)}%
</Text>
</View>
</View>
);
}
+143
View File
@@ -0,0 +1,143 @@
import React from 'react';
import { View, StyleSheet, TouchableOpacity, Platform, Dimensions } from 'react-native';
import { BottomTabBarProps } from '@react-navigation/bottom-tabs';
import { Ionicons } from '@expo/vector-icons';
import { LinearGradient } from 'expo-linear-gradient';
import { theme } from '../styles/theme';
import { Animated } from 'react-native';
const { width } = Dimensions.get('window');
export function CustomTabBar({ state, descriptors, navigation }: BottomTabBarProps) {
return (
<View style={styles.container}>
<LinearGradient
colors={['rgba(255, 255, 255, 0.9)', 'rgba(255, 255, 255, 0.7)']}
style={[styles.tabBar, theme.shadows.medium]}
start={{ x: 0, y: 0 }}
end={{ x: 0, y: 1 }}
>
{state.routes.map((route, index) => {
const { options } = descriptors[route.key];
const isFocused = state.index === index;
const onPress = () => {
const event = navigation.emit({
type: 'tabPress',
target: route.key,
canPreventDefault: true,
});
if (!isFocused && !event.defaultPrevented) {
navigation.navigate(route.name);
}
};
const getIconName = (routeName: string, focused: boolean): keyof typeof Ionicons.glyphMap => {
switch (routeName) {
case 'index':
return focused ? 'home' : 'home-outline';
case 'goals':
return focused ? 'trophy' : 'trophy-outline';
case 'attendance':
return focused ? 'calendar' : 'calendar-outline';
case 'recommendations':
return focused ? 'sparkles' : 'sparkles-outline';
case 'profile':
return focused ? 'person' : 'person-outline';
default:
return 'ellipse-outline';
}
};
// Animation for scale
const scaleValue = React.useRef(new Animated.Value(1)).current;
React.useEffect(() => {
Animated.spring(scaleValue, {
toValue: isFocused ? 1.2 : 1,
useNativeDriver: true,
friction: 8,
}).start();
}, [isFocused]);
return (
<TouchableOpacity
key={index}
accessibilityRole="button"
accessibilityState={isFocused ? { selected: true } : {}}
accessibilityLabel={options.tabBarAccessibilityLabel}
testID={(options as any).tabBarTestID}
onPress={onPress}
style={styles.tabItem}
activeOpacity={0.7}
>
<Animated.View style={{ transform: [{ scale: scaleValue }] }}>
{isFocused ? (
<LinearGradient
colors={theme.gradients.primary}
style={styles.iconContainer}
>
<Ionicons
name={getIconName(route.name, true)}
size={20}
color="#fff"
/>
</LinearGradient>
) : (
<Ionicons
name={getIconName(route.name, false)}
size={24}
color={theme.colors.gray500}
/>
)}
</Animated.View>
</TouchableOpacity>
);
})}
</LinearGradient>
</View>
);
}
const styles = StyleSheet.create({
container: {
position: 'absolute',
bottom: 0,
left: 0,
right: 0,
alignItems: 'center',
paddingBottom: Platform.OS === 'ios' ? 30 : 20,
pointerEvents: 'box-none',
},
tabBar: {
flexDirection: 'row',
backgroundColor: 'rgba(255, 255, 255, 0.8)',
borderRadius: 35,
height: 70,
width: width - 40,
justifyContent: 'space-around',
alignItems: 'center',
borderWidth: 1,
borderColor: 'rgba(255, 255, 255, 0.5)',
paddingHorizontal: 10,
},
tabItem: {
flex: 1,
alignItems: 'center',
justifyContent: 'center',
height: '100%',
},
iconContainer: {
width: 40,
height: 40,
borderRadius: 20,
alignItems: 'center',
justifyContent: 'center',
shadowColor: theme.colors.primary,
shadowOffset: { width: 0, height: 4 },
shadowOpacity: 0.3,
shadowRadius: 8,
elevation: 4,
},
});
+223
View File
@@ -0,0 +1,223 @@
import React, { useRef } from 'react';
import { View, Text, StyleSheet, Dimensions, ScrollView, TouchableOpacity, Animated } from 'react-native';
import { LinearGradient } from 'expo-linear-gradient';
import { Ionicons } from '@expo/vector-icons';
import { theme } from '../styles/theme';
import { CircularProgress } from './CircularProgress';
import type { FitnessGoal } from '../services/fitnessGoals';
const { width } = Dimensions.get('window');
const CARD_WIDTH = width * 0.8;
const SPACING = 20;
interface GoalCarouselProps {
goals: FitnessGoal[];
onGoalPress: (goal: FitnessGoal) => void;
}
export function GoalCarousel({ goals, onGoalPress }: GoalCarouselProps) {
const scrollX = useRef(new Animated.Value(0)).current;
if (goals.length === 0) {
return (
<View style={styles.emptyContainer}>
<Text style={styles.emptyText}>No active goals</Text>
</View>
);
}
return (
<View>
<Animated.ScrollView
horizontal
showsHorizontalScrollIndicator={false}
snapToInterval={CARD_WIDTH + SPACING}
decelerationRate="fast"
contentContainerStyle={styles.scrollContent}
onScroll={Animated.event(
[{ nativeEvent: { contentOffset: { x: scrollX } } }],
{ useNativeDriver: true }
)}
scrollEventThrottle={16}
>
{goals.map((goal, index) => {
const inputRange = [
(index - 1) * (CARD_WIDTH + SPACING),
index * (CARD_WIDTH + SPACING),
(index + 1) * (CARD_WIDTH + SPACING),
];
const scale = scrollX.interpolate({
inputRange,
outputRange: [0.9, 1, 0.9],
extrapolate: 'clamp',
});
const opacity = scrollX.interpolate({
inputRange,
outputRange: [0.7, 1, 0.7],
extrapolate: 'clamp',
});
return (
<TouchableOpacity
key={goal.id}
activeOpacity={0.9}
onPress={() => onGoalPress(goal)}
>
<Animated.View
style={[
styles.cardContainer,
{ transform: [{ scale }], opacity },
]}
>
<LinearGradient
colors={['#ffffff', '#f9fafb']}
style={[styles.card, theme.shadows.medium]}
>
<View style={styles.cardHeader}>
<View style={styles.iconContainer}>
<LinearGradient
colors={theme.gradients.primary}
style={styles.iconGradient}
>
<Ionicons name="trophy" size={24} color="#fff" />
</LinearGradient>
</View>
<View style={styles.priorityBadge}>
<Text style={styles.priorityText}>{goal.priority.toUpperCase()}</Text>
</View>
</View>
<Text style={styles.title} numberOfLines={1}>{goal.title}</Text>
<Text style={styles.description} numberOfLines={2}>{goal.description}</Text>
<View style={styles.progressContainer}>
<CircularProgress
progress={goal.progress || 0}
size={80}
strokeWidth={8}
color={theme.colors.primary}
/>
<View style={styles.progressDetails}>
<Text style={styles.progressValue}>
{goal.currentValue} / {goal.targetValue}
</Text>
<Text style={styles.progressUnit}>{goal.unit}</Text>
</View>
</View>
<View style={styles.footer}>
<Text style={styles.date}>
Target: {goal.targetDate ? new Date(goal.targetDate).toLocaleDateString() : 'No date'}
</Text>
</View>
</LinearGradient>
</Animated.View>
</TouchableOpacity>
);
})}
</Animated.ScrollView>
</View>
);
}
const styles = StyleSheet.create({
scrollContent: {
paddingHorizontal: (width - CARD_WIDTH) / 2,
paddingVertical: 20,
},
emptyContainer: {
height: 200,
justifyContent: 'center',
alignItems: 'center',
},
emptyText: {
color: theme.colors.gray500,
fontSize: 16,
},
cardContainer: {
width: CARD_WIDTH,
marginRight: SPACING,
},
card: {
borderRadius: 24,
padding: 24,
height: 320,
justifyContent: 'space-between',
borderWidth: 1,
borderColor: 'rgba(0,0,0,0.05)',
},
cardHeader: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'flex-start',
marginBottom: 16,
},
iconContainer: {
shadowColor: theme.colors.primary,
shadowOffset: { width: 0, height: 4 },
shadowOpacity: 0.3,
shadowRadius: 8,
elevation: 4,
},
iconGradient: {
width: 48,
height: 48,
borderRadius: 16,
justifyContent: 'center',
alignItems: 'center',
},
priorityBadge: {
backgroundColor: 'rgba(59, 130, 246, 0.1)',
paddingHorizontal: 12,
paddingVertical: 6,
borderRadius: 12,
},
priorityText: {
color: theme.colors.primary,
fontSize: 12,
fontWeight: '700',
},
title: {
fontSize: 24,
fontWeight: '800',
color: theme.colors.gray900,
marginBottom: 8,
},
description: {
fontSize: 14,
color: theme.colors.gray500,
marginBottom: 24,
lineHeight: 20,
},
progressContainer: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
marginBottom: 24,
},
progressDetails: {
alignItems: 'flex-end',
},
progressValue: {
fontSize: 24,
fontWeight: '700',
color: theme.colors.gray900,
},
progressUnit: {
fontSize: 14,
color: theme.colors.gray500,
fontWeight: '500',
},
footer: {
borderTopWidth: 1,
borderTopColor: theme.colors.gray100,
paddingTop: 16,
},
date: {
fontSize: 12,
color: theme.colors.gray400,
fontWeight: '500',
},
});
@@ -8,6 +8,7 @@ import {
TouchableOpacity,
ScrollView,
Platform,
Alert,
} from 'react-native';
import { Ionicons } from '@expo/vector-icons';
import DateTimePicker from '@react-native-community/datetimepicker';
@@ -59,7 +60,7 @@ export function GoalCreationModal({ visible, onClose, onSubmit }: GoalCreationMo
const handleSubmit = async () => {
if (!title.trim()) {
alert('Please enter a goal title');
Alert.alert('Error', 'Please enter a goal title');
return;
}
@@ -81,7 +82,7 @@ export function GoalCreationModal({ visible, onClose, onSubmit }: GoalCreationMo
onClose();
} catch (error) {
console.error('Error creating goal:', error);
alert('Failed to create goal. Please try again.');
Alert.alert('Error', 'Failed to create goal. Please try again.');
} finally {
setSubmitting(false);
}
@@ -203,28 +204,31 @@ export function GoalCreationModal({ visible, onClose, onSubmit }: GoalCreationMo
<Text style={styles.label}>Target Date</Text>
<TouchableOpacity
style={styles.dateButton}
onPress={() => setShowDatePicker(true)}
onPress={() => setShowDatePicker(!showDatePicker)}
>
<Text style={targetDate ? styles.dateText : styles.datePlaceholder}>
{targetDate ? targetDate.toLocaleDateString() : 'Select target date'}
</Text>
<Ionicons name="calendar-outline" size={20} color="#6b7280" />
<Ionicons name={showDatePicker ? "chevron-up" : "calendar-outline"} size={20} color="#6b7280" />
</TouchableOpacity>
</View>
{showDatePicker && (
<DateTimePicker
value={targetDate || new Date()}
mode="date"
display={Platform.OS === 'ios' ? 'spinner' : 'default'}
onChange={(event, selectedDate) => {
setShowDatePicker(Platform.OS === 'ios');
if (selectedDate) {
setTargetDate(selectedDate);
}
}}
minimumDate={new Date()}
/>
<View style={Platform.OS === 'ios' ? styles.datePickerContainer : undefined}>
<DateTimePicker
value={targetDate || new Date()}
mode="date"
display={Platform.OS === 'ios' ? 'inline' : 'default'}
onChange={(event, selectedDate) => {
setShowDatePicker(Platform.OS === 'ios');
if (selectedDate) {
setTargetDate(selectedDate);
}
}}
minimumDate={new Date()}
themeVariant="light"
/>
</View>
)}
{/* Priority */}
@@ -411,4 +415,12 @@ const styles = StyleSheet.create({
fontWeight: '600',
color: '#fff',
},
datePickerContainer: {
backgroundColor: '#fff',
borderRadius: 8,
borderWidth: 1,
borderColor: '#d1d5db',
marginTop: 8,
overflow: 'hidden',
},
});
@@ -0,0 +1,131 @@
import React from 'react';
import { StyleSheet, View, Animated, Dimensions, ImageSourcePropType } from 'react-native';
import { LinearGradient } from 'expo-linear-gradient';
import { theme } from '../styles/theme';
const { width } = Dimensions.get('window');
const HEADER_HEIGHT = 300;
interface ParallaxScrollViewProps {
children: React.ReactNode;
headerImage?: ImageSourcePropType;
headerBackgroundColor?: string;
headerContent?: React.ReactNode;
}
export function ParallaxScrollView({
children,
headerImage,
headerBackgroundColor = theme.colors.primary,
headerContent,
}: ParallaxScrollViewProps) {
const scrollY = React.useRef(new Animated.Value(0)).current;
const headerTranslateY = scrollY.interpolate({
inputRange: [0, HEADER_HEIGHT],
outputRange: [0, -HEADER_HEIGHT / 2],
extrapolate: 'clamp',
});
const imageScale = scrollY.interpolate({
inputRange: [-HEADER_HEIGHT, 0, HEADER_HEIGHT],
outputRange: [2, 1, 1],
extrapolate: 'clamp',
});
const headerOpacity = scrollY.interpolate({
inputRange: [0, HEADER_HEIGHT / 2],
outputRange: [1, 0],
extrapolate: 'clamp',
});
return (
<View style={styles.container}>
<Animated.ScrollView
contentContainerStyle={styles.scrollContent}
onScroll={Animated.event(
[{ nativeEvent: { contentOffset: { y: scrollY } } }],
{ useNativeDriver: true }
)}
scrollEventThrottle={16}
showsVerticalScrollIndicator={false}
>
<View style={styles.contentContainer}>{children}</View>
</Animated.ScrollView>
<Animated.View
style={[
styles.header,
{
transform: [{ translateY: headerTranslateY }],
},
]}
>
<Animated.View
style={[
StyleSheet.absoluteFill,
{
transform: [{ scale: imageScale }],
},
]}
>
<LinearGradient
colors={theme.gradients.primary}
style={StyleSheet.absoluteFill}
start={{ x: 0, y: 0 }}
end={{ x: 1, y: 1 }}
/>
{headerImage && (
<Animated.Image
source={headerImage}
style={[styles.headerImage, { opacity: 0.5 }]}
/>
)}
</Animated.View>
<Animated.View style={[styles.headerContent, { opacity: headerOpacity }]}>
{headerContent}
</Animated.View>
</Animated.View>
</View>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: theme.colors.background,
},
scrollContent: {
paddingTop: HEADER_HEIGHT,
},
contentContainer: {
backgroundColor: theme.colors.background,
borderTopLeftRadius: 32,
borderTopRightRadius: 32,
marginTop: -32,
paddingTop: 32,
minHeight: 800,
},
header: {
position: 'absolute',
top: 0,
left: 0,
right: 0,
height: HEADER_HEIGHT,
overflow: 'hidden',
zIndex: 1,
},
headerImage: {
...StyleSheet.absoluteFillObject,
width: undefined,
height: undefined,
resizeMode: 'cover',
},
headerContent: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
paddingBottom: 32,
},
});
@@ -0,0 +1,114 @@
import React from 'react';
import { View, Text, StyleSheet, TouchableOpacity, Dimensions } from 'react-native';
import { LinearGradient } from 'expo-linear-gradient';
import { Ionicons } from '@expo/vector-icons';
import { theme } from '../styles/theme';
const { width } = Dimensions.get('window');
const ITEM_WIDTH = (width - 40 - 16) / 2; // (Screen width - padding - gap) / 2
interface QuickActionProps {
icon: keyof typeof Ionicons.glyphMap;
label: string;
gradient: readonly [string, string, ...string[]];
onPress?: () => void;
}
function QuickActionItem({ icon, label, gradient, onPress }: QuickActionProps) {
return (
<TouchableOpacity onPress={onPress} activeOpacity={0.8} style={styles.itemContainer}>
<LinearGradient
colors={['rgba(255, 255, 255, 0.8)', 'rgba(255, 255, 255, 0.6)']}
style={[styles.item, theme.shadows.subtle]}
>
<LinearGradient
colors={gradient}
style={styles.iconContainer}
start={{ x: 0, y: 0 }}
end={{ x: 1, y: 1 }}
>
<Ionicons name={icon} size={24} color="#fff" />
</LinearGradient>
<Text style={styles.label}>{label}</Text>
<Ionicons name="chevron-forward" size={16} color={theme.colors.gray400} style={styles.arrow} />
</LinearGradient>
</TouchableOpacity>
);
}
export function QuickActionGrid() {
return (
<View style={styles.container}>
<Text style={styles.sectionTitle}>Quick Actions</Text>
<View style={styles.grid}>
<QuickActionItem
icon="barbell"
label="Log Workout"
gradient={theme.gradients.primary}
/>
<QuickActionItem
icon="restaurant"
label="Track Meal"
gradient={theme.gradients.success}
/>
<QuickActionItem
icon="water"
label="Add Water"
gradient={theme.gradients.ocean}
/>
<QuickActionItem
icon="scan"
label="Scan Food"
gradient={theme.gradients.purple}
/>
</View>
</View>
);
}
const styles = StyleSheet.create({
container: {
paddingHorizontal: 20,
marginBottom: 24,
},
sectionTitle: {
fontSize: 18,
fontWeight: '700',
color: theme.colors.gray900,
marginBottom: 16,
},
grid: {
flexDirection: 'row',
flexWrap: 'wrap',
gap: 16,
},
itemContainer: {
width: ITEM_WIDTH,
},
item: {
flexDirection: 'row',
alignItems: 'center',
padding: 16,
borderRadius: 20,
backgroundColor: '#fff',
borderWidth: 1,
borderColor: 'rgba(255, 255, 255, 0.6)',
},
iconContainer: {
width: 40,
height: 40,
borderRadius: 14,
justifyContent: 'center',
alignItems: 'center',
marginRight: 12,
},
label: {
fontSize: 14,
fontWeight: '600',
color: theme.colors.gray800,
flex: 1,
},
arrow: {
opacity: 0.5,
},
});