claude take :(

This commit is contained in:
dimitar committed 2025-11-26 01:57:33 +01:00
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,
},
});
+155 -113
View File
@@ -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,
},
});