UI Updates and Bug Fixes
This commit is contained in:
@@ -0,0 +1,105 @@
|
||||
import React from 'react';
|
||||
import { TouchableOpacity, TouchableOpacityProps, ActivityIndicator, GestureResponderEvent, StyleProp, ViewStyle, StyleSheet } from 'react-native';
|
||||
import { Typography } from './Typography';
|
||||
|
||||
interface ButtonProps extends TouchableOpacityProps {
|
||||
variant?: 'primary' | 'secondary' | 'outline' | 'ghost';
|
||||
title: string;
|
||||
loading?: boolean;
|
||||
onPress?: (event: GestureResponderEvent) => void;
|
||||
disabled?: boolean;
|
||||
style?: StyleProp<ViewStyle>;
|
||||
}
|
||||
|
||||
export const Button: React.FC<ButtonProps> = ({
|
||||
variant = 'primary',
|
||||
title,
|
||||
loading = false,
|
||||
disabled,
|
||||
style,
|
||||
...props
|
||||
}) => {
|
||||
const getBackgroundStyle = () => {
|
||||
if (disabled) return styles.bgDisabled;
|
||||
switch (variant) {
|
||||
case 'primary':
|
||||
return styles.bgPrimary;
|
||||
case 'secondary':
|
||||
return styles.bgSecondary;
|
||||
case 'outline':
|
||||
return styles.bgOutline;
|
||||
case 'ghost':
|
||||
return styles.bgGhost;
|
||||
default:
|
||||
return styles.bgPrimary;
|
||||
}
|
||||
};
|
||||
|
||||
const getTextStyle = () => {
|
||||
if (disabled) return styles.textDisabled;
|
||||
switch (variant) {
|
||||
case 'primary':
|
||||
case 'secondary':
|
||||
return styles.textWhite;
|
||||
case 'outline':
|
||||
case 'ghost':
|
||||
return styles.textPrimary;
|
||||
default:
|
||||
return styles.textWhite;
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<TouchableOpacity
|
||||
style={[styles.container, getBackgroundStyle(), style]}
|
||||
disabled={disabled || loading}
|
||||
activeOpacity={0.8}
|
||||
{...props}
|
||||
>
|
||||
{loading ? (
|
||||
<ActivityIndicator color={variant === 'outline' || variant === 'ghost' ? '#4F46E5' : '#FFFFFF'} />
|
||||
) : (
|
||||
<Typography weight="medium" style={getTextStyle()}>
|
||||
{title}
|
||||
</Typography>
|
||||
)}
|
||||
</TouchableOpacity>
|
||||
);
|
||||
};
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
paddingVertical: 12,
|
||||
paddingHorizontal: 24,
|
||||
borderRadius: 12,
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
},
|
||||
bgDisabled: {
|
||||
backgroundColor: '#D1D5DB',
|
||||
},
|
||||
bgPrimary: {
|
||||
backgroundColor: '#4F46E5',
|
||||
},
|
||||
bgSecondary: {
|
||||
backgroundColor: '#10B981',
|
||||
},
|
||||
bgOutline: {
|
||||
backgroundColor: 'transparent',
|
||||
borderColor: '#4F46E5',
|
||||
borderWidth: 2,
|
||||
},
|
||||
bgGhost: {
|
||||
backgroundColor: 'transparent',
|
||||
},
|
||||
textDisabled: {
|
||||
color: '#6B7280',
|
||||
},
|
||||
textWhite: {
|
||||
color: '#FFFFFF',
|
||||
},
|
||||
textPrimary: {
|
||||
color: '#4F46E5',
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,45 @@
|
||||
import React from 'react';
|
||||
import { View, ViewProps, StyleProp, ViewStyle, StyleSheet } from 'react-native';
|
||||
|
||||
interface CardProps extends ViewProps {
|
||||
children: React.ReactNode;
|
||||
noPadding?: boolean;
|
||||
style?: StyleProp<ViewStyle>;
|
||||
}
|
||||
|
||||
export const Card: React.FC<CardProps> = ({
|
||||
children,
|
||||
noPadding = false,
|
||||
style,
|
||||
...props
|
||||
}) => {
|
||||
return (
|
||||
<View
|
||||
style={[
|
||||
styles.card,
|
||||
noPadding ? null : styles.padding,
|
||||
style
|
||||
]}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
card: {
|
||||
backgroundColor: '#FFFFFF',
|
||||
borderRadius: 16,
|
||||
borderWidth: 1,
|
||||
borderColor: '#F3F4F6', // gray-100
|
||||
shadowColor: '#000',
|
||||
shadowOffset: { width: 0, height: 2 },
|
||||
shadowOpacity: 0.05,
|
||||
shadowRadius: 15,
|
||||
elevation: 2,
|
||||
},
|
||||
padding: {
|
||||
padding: 16, // p-4
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,72 @@
|
||||
import React from 'react';
|
||||
import { View, TouchableOpacity, StyleSheet } from 'react-native';
|
||||
import { useNavigation } from '@react-navigation/native';
|
||||
import { ChevronLeft } from 'lucide-react-native';
|
||||
import { Typography } from './Typography';
|
||||
import { useResponsiveLayout } from '../../hooks/useResponsiveLayout';
|
||||
import { THEME } from '../../constants/theme';
|
||||
|
||||
interface HeaderProps {
|
||||
title: string;
|
||||
showBack?: boolean;
|
||||
rightComponent?: React.ReactNode;
|
||||
}
|
||||
|
||||
export const Header: React.FC<HeaderProps> = ({ title, showBack = false, rightComponent }) => {
|
||||
const navigation = useNavigation();
|
||||
const { screenPadding } = useResponsiveLayout();
|
||||
|
||||
return (
|
||||
<View style={[styles.header, { paddingHorizontal: screenPadding }]}>
|
||||
<View style={styles.leftContainer}>
|
||||
{showBack && (
|
||||
<TouchableOpacity
|
||||
onPress={() => navigation.goBack()}
|
||||
style={styles.backButton}
|
||||
hitSlop={{ top: 10, bottom: 10, left: 10, right: 10 }}
|
||||
>
|
||||
<ChevronLeft color="#FFF" size={26} strokeWidth={2.5} />
|
||||
</TouchableOpacity>
|
||||
)}
|
||||
<Typography variant="h3" weight="bold" style={styles.title}>
|
||||
{title}
|
||||
</Typography>
|
||||
</View>
|
||||
{rightComponent && (
|
||||
<View style={styles.rightContainer}>
|
||||
{rightComponent}
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
header: {
|
||||
paddingHorizontal: 16,
|
||||
paddingVertical: 16,
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
backgroundColor: THEME.colors.primary, // Primary brand color
|
||||
},
|
||||
leftContainer: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
},
|
||||
rightContainer: {
|
||||
justifyContent: 'center',
|
||||
},
|
||||
backButton: {
|
||||
marginRight: 12,
|
||||
width: 40,
|
||||
height: 40,
|
||||
borderRadius: 20,
|
||||
backgroundColor: 'rgba(255,255,255,0.2)',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
},
|
||||
title: {
|
||||
color: '#FFFFFF',
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,70 @@
|
||||
import React from 'react';
|
||||
import { TextInput, TextInputProps, View, StyleSheet } from 'react-native';
|
||||
import { Typography } from './Typography';
|
||||
|
||||
export interface InputProps extends TextInputProps {
|
||||
label?: string;
|
||||
error?: string;
|
||||
placeholder?: string;
|
||||
keyboardType?: 'default' | 'email-address' | 'numeric' | 'phone-pad';
|
||||
autoCapitalize?: 'none' | 'sentences' | 'words' | 'characters';
|
||||
secureTextEntry?: boolean;
|
||||
value?: string;
|
||||
onChangeText?: (text: string) => void;
|
||||
style?: any;
|
||||
labelStyle?: any;
|
||||
}
|
||||
|
||||
export function Input({ label, error, style, labelStyle, ...props }: InputProps) {
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
{label && (
|
||||
<Typography variant="caption" weight="medium" style={[styles.label, labelStyle]}>
|
||||
{label}
|
||||
</Typography>
|
||||
)}
|
||||
<TextInput
|
||||
style={[
|
||||
styles.input,
|
||||
error ? styles.inputError : null,
|
||||
style,
|
||||
]}
|
||||
placeholderTextColor="#9CA3AF"
|
||||
{...props}
|
||||
/>
|
||||
{error && (
|
||||
<Typography variant="caption" style={styles.errorText}>
|
||||
{error}
|
||||
</Typography>
|
||||
)}
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
marginBottom: 16,
|
||||
width: '100%',
|
||||
},
|
||||
label: {
|
||||
marginBottom: 6,
|
||||
color: '#374151', // gray-700
|
||||
},
|
||||
input: {
|
||||
backgroundColor: '#FFFFFF',
|
||||
borderWidth: 1,
|
||||
borderColor: '#D1D5DB', // gray-300
|
||||
borderRadius: 12,
|
||||
paddingHorizontal: 16,
|
||||
paddingVertical: 14,
|
||||
fontSize: 16,
|
||||
color: '#111827', // gray-900
|
||||
},
|
||||
inputError: {
|
||||
borderColor: '#EF4444', // red-500
|
||||
},
|
||||
errorText: {
|
||||
color: '#EF4444',
|
||||
marginTop: 4,
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,92 @@
|
||||
import React from 'react';
|
||||
import { Text, TextProps, StyleProp, TextStyle, StyleSheet } from 'react-native';
|
||||
import { useResponsiveLayout } from '../../hooks/useResponsiveLayout';
|
||||
|
||||
interface TypographyProps extends TextProps {
|
||||
variant?: 'h1' | 'h2' | 'h3' | 'body' | 'caption';
|
||||
weight?: 'normal' | 'medium' | 'bold';
|
||||
children: React.ReactNode;
|
||||
style?: StyleProp<TextStyle>;
|
||||
}
|
||||
|
||||
export const Typography: React.FC<TypographyProps> = ({
|
||||
variant = 'body',
|
||||
weight = 'normal',
|
||||
children,
|
||||
style,
|
||||
...props
|
||||
}) => {
|
||||
const { scale } = useResponsiveLayout();
|
||||
|
||||
const getVariantStyles = () => {
|
||||
switch (variant) {
|
||||
case 'h1': return styles.h1;
|
||||
case 'h2': return styles.h2;
|
||||
case 'h3': return styles.h3;
|
||||
case 'caption': return styles.caption;
|
||||
case 'body':
|
||||
default: return styles.body;
|
||||
}
|
||||
};
|
||||
|
||||
const getWeightStyles = () => {
|
||||
switch (weight) {
|
||||
case 'bold': return styles.bold;
|
||||
case 'medium': return styles.medium;
|
||||
case 'normal':
|
||||
default: return styles.normal;
|
||||
}
|
||||
};
|
||||
|
||||
const variantStyle = StyleSheet.flatten(getVariantStyles());
|
||||
const scaledStyle = {
|
||||
fontSize: scale(variantStyle.fontSize as number),
|
||||
lineHeight: scale(variantStyle.lineHeight as number),
|
||||
};
|
||||
|
||||
return (
|
||||
<Text
|
||||
style={[variantStyle, scaledStyle, getWeightStyles(), style]}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</Text>
|
||||
);
|
||||
};
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
h1: {
|
||||
fontSize: 30,
|
||||
lineHeight: 36,
|
||||
color: '#111827', // text-gray-900
|
||||
},
|
||||
h2: {
|
||||
fontSize: 24,
|
||||
lineHeight: 32,
|
||||
color: '#111827', // text-gray-900
|
||||
},
|
||||
h3: {
|
||||
fontSize: 20,
|
||||
lineHeight: 28,
|
||||
color: '#111827', // text-gray-900
|
||||
},
|
||||
body: {
|
||||
fontSize: 16,
|
||||
lineHeight: 24,
|
||||
color: '#111827', // text-gray-900
|
||||
},
|
||||
caption: {
|
||||
fontSize: 14,
|
||||
lineHeight: 20,
|
||||
color: '#6B7280', // text-gray-500
|
||||
},
|
||||
bold: {
|
||||
fontWeight: 'bold',
|
||||
},
|
||||
medium: {
|
||||
fontWeight: '500',
|
||||
},
|
||||
normal: {
|
||||
fontWeight: 'normal',
|
||||
},
|
||||
});
|
||||
Reference in New Issue
Block a user