claude take :(
This commit is contained in:
1 parent
fc12cecd30
commit
73d2c4c1ed
13 files changed
+1460
-517
No files matched your search
@@ -0,0 +1,136 @@
|
||||
/**
|
||||
* Animated Button Component
|
||||
* Modern button with gradient backgrounds and smooth animations
|
||||
*/
|
||||
|
||||
import React, { useRef } from 'react';
|
||||
import {
|
||||
TouchableOpacity,
|
||||
Text,
|
||||
StyleSheet,
|
||||
Animated,
|
||||
ActivityIndicator,
|
||||
ViewStyle,
|
||||
TextStyle,
|
||||
} from 'react-native';
|
||||
import { LinearGradient } from 'expo-linear-gradient';
|
||||
import { theme } from '../styles/theme';
|
||||
|
||||
interface AnimatedButtonProps {
|
||||
onPress: () => void;
|
||||
title: string;
|
||||
variant?: 'primary' | 'secondary' | 'danger' | 'success';
|
||||
loading?: boolean;
|
||||
disabled?: boolean;
|
||||
style?: ViewStyle;
|
||||
textStyle?: TextStyle;
|
||||
icon?: React.ReactNode;
|
||||
}
|
||||
|
||||
export function AnimatedButton({
|
||||
onPress,
|
||||
title,
|
||||
variant = 'primary',
|
||||
loading = false,
|
||||
disabled = false,
|
||||
style,
|
||||
textStyle,
|
||||
icon,
|
||||
}: AnimatedButtonProps) {
|
||||
const scaleAnim = useRef(new Animated.Value(1)).current;
|
||||
|
||||
const handlePressIn = () => {
|
||||
Animated.spring(scaleAnim, {
|
||||
toValue: 0.95,
|
||||
friction: 8,
|
||||
tension: 100,
|
||||
useNativeDriver: true,
|
||||
}).start();
|
||||
};
|
||||
|
||||
const handlePressOut = () => {
|
||||
Animated.spring(scaleAnim, {
|
||||
toValue: 1,
|
||||
friction: 8,
|
||||
tension: 100,
|
||||
useNativeDriver: true,
|
||||
}).start();
|
||||
};
|
||||
|
||||
const getGradientColors = (): readonly [string, string, ...string[]] => {
|
||||
switch (variant) {
|
||||
case 'primary':
|
||||
return theme.gradients.primary;
|
||||
case 'danger':
|
||||
return theme.gradients.danger;
|
||||
case 'success':
|
||||
return theme.gradients.success;
|
||||
case 'secondary':
|
||||
return [theme.colors.gray600, theme.colors.gray700] as const;
|
||||
default:
|
||||
return theme.gradients.primary;
|
||||
}
|
||||
};
|
||||
|
||||
const getShadowStyle = () => {
|
||||
if (disabled) return {};
|
||||
switch (variant) {
|
||||
case 'danger':
|
||||
return theme.shadows.glowDanger;
|
||||
default:
|
||||
return theme.shadows.glow;
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Animated.View style={[{ transform: [{ scale: scaleAnim }] }, style]}>
|
||||
<TouchableOpacity
|
||||
onPress={onPress}
|
||||
onPressIn={handlePressIn}
|
||||
onPressOut={handlePressOut}
|
||||
disabled={disabled || loading}
|
||||
activeOpacity={0.9}
|
||||
>
|
||||
<LinearGradient
|
||||
colors={getGradientColors()}
|
||||
start={{ x: 0, y: 0 }}
|
||||
end={{ x: 1, y: 0 }}
|
||||
style={[
|
||||
styles.button,
|
||||
getShadowStyle(),
|
||||
disabled && styles.disabled,
|
||||
]}
|
||||
>
|
||||
{loading ? (
|
||||
<ActivityIndicator color="#fff" />
|
||||
) : (
|
||||
<>
|
||||
{icon}
|
||||
<Text style={[styles.text, textStyle]}>{title}</Text>
|
||||
</>
|
||||
)}
|
||||
</LinearGradient>
|
||||
</TouchableOpacity>
|
||||
</Animated.View>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
button: {
|
||||
paddingVertical: 16,
|
||||
paddingHorizontal: 24,
|
||||
borderRadius: theme.borderRadius.lg,
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
gap: 8,
|
||||
},
|
||||
text: {
|
||||
color: theme.colors.white,
|
||||
fontSize: theme.typography.fontSize.base,
|
||||
fontWeight: theme.typography.fontWeight.semibold,
|
||||
},
|
||||
disabled: {
|
||||
opacity: 0.5,
|
||||
},
|
||||
});
|
||||
@@ -1,7 +1,9 @@
|
||||
import React from 'react';
|
||||
import { View, Text, StyleSheet, TouchableOpacity, Alert } from 'react-native';
|
||||
import { Ionicons } from '@expo/vector-icons';
|
||||
import { LinearGradient } from 'expo-linear-gradient';
|
||||
import type { FitnessGoal } from '../services/fitnessGoals';
|
||||
import { theme } from '../styles/theme';
|
||||
|
||||
interface GoalProgressCardProps {
|
||||
goal: FitnessGoal;
|
||||
@@ -30,12 +32,12 @@ export function GoalProgressCard({ goal, onPress, onComplete, onDelete }: GoalPr
|
||||
}
|
||||
};
|
||||
|
||||
const getPriorityColor = (priority: string) => {
|
||||
const getPriorityGradient = (priority: string): readonly [string, string] => {
|
||||
switch (priority) {
|
||||
case 'high': return '#ef4444';
|
||||
case 'medium': return '#f59e0b';
|
||||
case 'low': return '#10b981';
|
||||
default: return '#6b7280';
|
||||
case 'high': return theme.gradients.danger;
|
||||
case 'medium': return theme.gradients.warning;
|
||||
case 'low': return theme.gradients.success;
|
||||
default: return theme.gradients.primary;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -52,134 +54,175 @@ export function GoalProgressCard({ goal, onPress, onComplete, onDelete }: GoalPr
|
||||
|
||||
return (
|
||||
<TouchableOpacity
|
||||
style={[styles.card, isCompleted && styles.cardCompleted]}
|
||||
onPress={onPress}
|
||||
activeOpacity={0.7}
|
||||
>
|
||||
<View style={styles.header}>
|
||||
<View style={styles.titleRow}>
|
||||
<Ionicons
|
||||
name={getGoalTypeIcon(goal.goalType) as any}
|
||||
size={24}
|
||||
color={isCompleted ? '#9ca3af' : '#2563eb'}
|
||||
/>
|
||||
<View style={styles.titleContainer}>
|
||||
<Text style={[styles.title, isCompleted && styles.titleCompleted]}>
|
||||
{goal.title}
|
||||
</Text>
|
||||
{goal.description && (
|
||||
<Text style={styles.description} numberOfLines={2}>
|
||||
{goal.description}
|
||||
<LinearGradient
|
||||
colors={isCompleted
|
||||
? ['rgba(16, 185, 129, 0.05)', 'rgba(5, 150, 105, 0.02)'] as const
|
||||
: ['rgba(255, 255, 255, 1)', 'rgba(249, 250, 251, 1)'] as const
|
||||
}
|
||||
style={[
|
||||
styles.card,
|
||||
theme.shadows.medium,
|
||||
isCompleted && styles.cardCompleted
|
||||
]}
|
||||
>
|
||||
{/* Priority Accent Bar */}
|
||||
<LinearGradient
|
||||
colors={getPriorityGradient(goal.priority)}
|
||||
start={{ x: 0, y: 0 }}
|
||||
end={{ x: 0, y: 1 }}
|
||||
style={styles.priorityAccent}
|
||||
/>
|
||||
|
||||
<View style={styles.header}>
|
||||
<View style={styles.titleRow}>
|
||||
<LinearGradient
|
||||
colors={isCompleted ? theme.gradients.success : getPriorityGradient(goal.priority)}
|
||||
style={styles.iconContainer}
|
||||
>
|
||||
<Ionicons
|
||||
name={getGoalTypeIcon(goal.goalType) as any}
|
||||
size={20}
|
||||
color="#fff"
|
||||
/>
|
||||
</LinearGradient>
|
||||
<View style={styles.titleContainer}>
|
||||
<Text style={[styles.title, isCompleted && styles.titleCompleted]}>
|
||||
{goal.title}
|
||||
</Text>
|
||||
{goal.description && (
|
||||
<Text style={styles.description} numberOfLines={2}>
|
||||
{goal.description}
|
||||
</Text>
|
||||
)}
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<View style={styles.actions}>
|
||||
{!isCompleted && onComplete && (
|
||||
<TouchableOpacity onPress={onComplete} style={styles.actionButton}>
|
||||
<Ionicons name="checkmark-circle-outline" size={24} color={theme.colors.success} />
|
||||
</TouchableOpacity>
|
||||
)}
|
||||
{onDelete && (
|
||||
<TouchableOpacity onPress={handleDelete} style={styles.actionButton}>
|
||||
<Ionicons name="trash-outline" size={22} color={theme.colors.danger} />
|
||||
</TouchableOpacity>
|
||||
)}
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<View style={styles.actions}>
|
||||
{!isCompleted && onComplete && (
|
||||
<TouchableOpacity onPress={onComplete} style={styles.actionButton}>
|
||||
<Ionicons name="checkmark-circle-outline" size={24} color="#10b981" />
|
||||
</TouchableOpacity>
|
||||
)}
|
||||
{onDelete && (
|
||||
<TouchableOpacity onPress={handleDelete} style={styles.actionButton}>
|
||||
<Ionicons name="trash-outline" size={22} color="#ef4444" />
|
||||
</TouchableOpacity>
|
||||
)}
|
||||
</View>
|
||||
</View>
|
||||
{goal.targetValue && (
|
||||
<View style={styles.progressSection}>
|
||||
<View style={styles.progressInfo}>
|
||||
<Text style={styles.progressText}>
|
||||
{goal.currentValue || 0} / {goal.targetValue} {goal.unit || ''}
|
||||
</Text>
|
||||
<Text style={[styles.progressPercentage, isCompleted && { color: theme.colors.success }]}>
|
||||
{progress.toFixed(0)}%
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
{goal.targetValue && (
|
||||
<View style={styles.progressSection}>
|
||||
<View style={styles.progressInfo}>
|
||||
<Text style={styles.progressText}>
|
||||
{goal.currentValue || 0} / {goal.targetValue} {goal.unit || ''}
|
||||
<View style={styles.progressBarContainer}>
|
||||
<LinearGradient
|
||||
colors={isCompleted ? theme.gradients.success : getPriorityGradient(goal.priority)}
|
||||
start={{ x: 0, y: 0 }}
|
||||
end={{ x: 1, y: 0 }}
|
||||
style={[
|
||||
styles.progressBar,
|
||||
{ width: `${Math.min(progress, 100)}%` }
|
||||
]}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
)}
|
||||
|
||||
<View style={styles.footer}>
|
||||
<LinearGradient
|
||||
colors={getPriorityGradient(goal.priority)}
|
||||
style={styles.priorityBadge}
|
||||
>
|
||||
<Text style={styles.priorityText}>{goal.priority.toUpperCase()}</Text>
|
||||
</LinearGradient>
|
||||
|
||||
{daysRemaining !== null && !isCompleted && (
|
||||
<Text style={[styles.daysRemaining, daysRemaining < 0 && styles.overdue]}>
|
||||
{daysRemaining < 0
|
||||
? `${Math.abs(daysRemaining)} days overdue`
|
||||
: `${daysRemaining} days remaining`
|
||||
}
|
||||
</Text>
|
||||
<Text style={styles.progressPercentage}>{progress.toFixed(0)}%</Text>
|
||||
</View>
|
||||
)}
|
||||
|
||||
<View style={styles.progressBarContainer}>
|
||||
<View
|
||||
style={[
|
||||
styles.progressBar,
|
||||
{ width: `${Math.min(progress, 100)}%` },
|
||||
isCompleted && styles.progressBarCompleted
|
||||
]}
|
||||
/>
|
||||
</View>
|
||||
{isCompleted && goal.completedDate && (
|
||||
<Text style={styles.completedDate}>
|
||||
Completed {new Date(goal.completedDate).toLocaleDateString()}
|
||||
</Text>
|
||||
)}
|
||||
</View>
|
||||
)}
|
||||
|
||||
<View style={styles.footer}>
|
||||
<View style={[styles.priorityBadge, { backgroundColor: getPriorityColor(goal.priority) }]}>
|
||||
<Text style={styles.priorityText}>{goal.priority.toUpperCase()}</Text>
|
||||
</View>
|
||||
|
||||
{daysRemaining !== null && !isCompleted && (
|
||||
<Text style={[styles.daysRemaining, daysRemaining < 0 && styles.overdue]}>
|
||||
{daysRemaining < 0
|
||||
? `${Math.abs(daysRemaining)} days overdue`
|
||||
: `${daysRemaining} days remaining`
|
||||
}
|
||||
</Text>
|
||||
)}
|
||||
|
||||
{isCompleted && goal.completedDate && (
|
||||
<Text style={styles.completedDate}>
|
||||
Completed {new Date(goal.completedDate).toLocaleDateString()}
|
||||
</Text>
|
||||
)}
|
||||
</View>
|
||||
</LinearGradient>
|
||||
</TouchableOpacity>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
card: {
|
||||
backgroundColor: '#fff',
|
||||
borderRadius: 12,
|
||||
borderRadius: theme.borderRadius.xl,
|
||||
padding: 16,
|
||||
marginBottom: 12,
|
||||
shadowColor: '#000',
|
||||
shadowOffset: { width: 0, height: 2 },
|
||||
shadowOpacity: 0.1,
|
||||
shadowRadius: 4,
|
||||
elevation: 3,
|
||||
borderWidth: 1,
|
||||
borderColor: 'rgba(59, 130, 246, 0.1)',
|
||||
overflow: 'hidden',
|
||||
},
|
||||
cardCompleted: {
|
||||
backgroundColor: '#f0fdf4',
|
||||
borderColor: '#bbf7d0',
|
||||
borderWidth: 1,
|
||||
borderColor: 'rgba(16, 185, 129, 0.2)',
|
||||
},
|
||||
priorityAccent: {
|
||||
position: 'absolute',
|
||||
left: 0,
|
||||
top: 0,
|
||||
bottom: 0,
|
||||
width: 4,
|
||||
},
|
||||
header: {
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'flex-start',
|
||||
marginBottom: 12,
|
||||
marginLeft: 8,
|
||||
},
|
||||
titleRow: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'flex-start',
|
||||
flex: 1,
|
||||
},
|
||||
iconContainer: {
|
||||
width: 40,
|
||||
height: 40,
|
||||
borderRadius: 20,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
marginRight: 12,
|
||||
},
|
||||
titleContainer: {
|
||||
marginLeft: 12,
|
||||
flex: 1,
|
||||
},
|
||||
title: {
|
||||
fontSize: 18,
|
||||
fontWeight: '600',
|
||||
color: '#111827',
|
||||
fontSize: theme.typography.fontSize.lg,
|
||||
fontWeight: theme.typography.fontWeight.semibold,
|
||||
color: theme.colors.gray900,
|
||||
marginBottom: 4,
|
||||
},
|
||||
titleCompleted: {
|
||||
color: '#9ca3af',
|
||||
color: theme.colors.gray600,
|
||||
textDecorationLine: 'line-through',
|
||||
},
|
||||
description: {
|
||||
fontSize: 14,
|
||||
color: '#6b7280',
|
||||
lineHeight: 20,
|
||||
fontSize: theme.typography.fontSize.sm,
|
||||
color: theme.colors.gray600,
|
||||
lineHeight: 18,
|
||||
},
|
||||
actions: {
|
||||
flexDirection: 'row',
|
||||
@@ -190,6 +233,7 @@ const styles = StyleSheet.create({
|
||||
},
|
||||
progressSection: {
|
||||
marginBottom: 12,
|
||||
marginLeft: 8,
|
||||
},
|
||||
progressInfo: {
|
||||
flexDirection: 'row',
|
||||
@@ -197,55 +241,53 @@ const styles = StyleSheet.create({
|
||||
marginBottom: 8,
|
||||
},
|
||||
progressText: {
|
||||
fontSize: 14,
|
||||
fontWeight: '500',
|
||||
color: '#374151',
|
||||
fontSize: theme.typography.fontSize.sm,
|
||||
fontWeight: theme.typography.fontWeight.medium,
|
||||
color: theme.colors.gray700,
|
||||
},
|
||||
progressPercentage: {
|
||||
fontSize: 14,
|
||||
fontWeight: '600',
|
||||
color: '#2563eb',
|
||||
fontSize: theme.typography.fontSize.sm,
|
||||
fontWeight: theme.typography.fontWeight.semibold,
|
||||
color: theme.colors.primary,
|
||||
},
|
||||
progressBarContainer: {
|
||||
height: 8,
|
||||
backgroundColor: '#e5e7eb',
|
||||
backgroundColor: theme.colors.gray200,
|
||||
borderRadius: 4,
|
||||
overflow: 'hidden',
|
||||
},
|
||||
progressBar: {
|
||||
height: '100%',
|
||||
backgroundColor: '#2563eb',
|
||||
borderRadius: 4,
|
||||
},
|
||||
progressBarCompleted: {
|
||||
backgroundColor: '#10b981',
|
||||
},
|
||||
footer: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
marginLeft: 8,
|
||||
},
|
||||
priorityBadge: {
|
||||
paddingHorizontal: 8,
|
||||
paddingVertical: 4,
|
||||
borderRadius: 4,
|
||||
paddingHorizontal: 10,
|
||||
paddingVertical: 5,
|
||||
borderRadius: theme.borderRadius.md,
|
||||
},
|
||||
priorityText: {
|
||||
fontSize: 10,
|
||||
fontWeight: '600',
|
||||
color: '#fff',
|
||||
fontSize: theme.typography.fontSize.xs,
|
||||
fontWeight: theme.typography.fontWeight.semibold,
|
||||
color: theme.colors.white,
|
||||
},
|
||||
daysRemaining: {
|
||||
fontSize: 12,
|
||||
color: '#6b7280',
|
||||
fontSize: theme.typography.fontSize.xs,
|
||||
color: theme.colors.gray600,
|
||||
fontWeight: theme.typography.fontWeight.medium,
|
||||
},
|
||||
overdue: {
|
||||
color: '#ef4444',
|
||||
fontWeight: '600',
|
||||
color: theme.colors.danger,
|
||||
fontWeight: theme.typography.fontWeight.semibold,
|
||||
},
|
||||
completedDate: {
|
||||
fontSize: 12,
|
||||
color: '#10b981',
|
||||
fontWeight: '500',
|
||||
fontSize: theme.typography.fontSize.xs,
|
||||
color: theme.colors.success,
|
||||
fontWeight: theme.typography.fontWeight.medium,
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,46 @@
|
||||
/**
|
||||
* Gradient Background Component
|
||||
* Reusable gradient background with multiple presets
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { StyleSheet, ViewStyle } from 'react-native';
|
||||
import { LinearGradient } from 'expo-linear-gradient';
|
||||
import { theme } from '../styles/theme';
|
||||
|
||||
interface GradientBackgroundProps {
|
||||
variant?: 'primary' | 'success' | 'warning' | 'danger' | 'purple' | 'ocean' | 'sunset' | 'dark';
|
||||
colors?: string[];
|
||||
style?: ViewStyle;
|
||||
children?: React.ReactNode;
|
||||
start?: { x: number; y: number };
|
||||
end?: { x: number; y: number };
|
||||
}
|
||||
|
||||
export function GradientBackground({
|
||||
variant = 'primary',
|
||||
colors,
|
||||
style,
|
||||
children,
|
||||
start = { x: 0, y: 0 },
|
||||
end = { x: 1, y: 1 },
|
||||
}: GradientBackgroundProps) {
|
||||
const gradientColors = (colors || theme.gradients[variant]) as readonly [string, string, ...string[]];
|
||||
|
||||
return (
|
||||
<LinearGradient
|
||||
colors={gradientColors}
|
||||
start={start}
|
||||
end={end}
|
||||
style={[styles.gradient, style]}
|
||||
>
|
||||
{children}
|
||||
</LinearGradient>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
gradient: {
|
||||
flex: 1,
|
||||
},
|
||||
});
|
||||
Reference in new issue
Block a user