Commit b891c912 by tungnq

TODO: Đã hoàn thiện màn hình giao diện học sinh chi tiết với checkbox theo dõi trạng thái

parent b20d3c80
const images = {
iconWarn: require('./images/iconWarn.png'),
iconSuccess: require('./images/iconSuccess.png'),
......@@ -60,7 +58,8 @@ const images = {
icSearchHeader:require('./icon/icon_png/icon_search_header.png'),
icSearch: require('./icon/icon_png/icon_search.png'),
icDocument: require('./icon/icon_png/ic_document.png'),
icTick: require('./icon/icon_png/ic_tick.png'),
//Image Logo
igLogo: require('./images/logo.png'),
igBackground: require('./images/background_slider_home.jpg')
......
import React, { useCallback, useMemo, useState, useEffect } from 'react';
import {
TouchableOpacity,
View,
Text,
StyleSheet,
Animated,
Image
} from 'react-native';
import R from '../assets/R';
const IconTick = R.images.icTick;
// COMPONENT: TickIcon - Hiển thị biểu tượng tick với animation
const TickIcon = React.memo(({ visible, size, color }) => {
// PERFORMANCE: Chỉ tạo animatedValue một lần khi component mount
const animatedValue = useMemo(() => new Animated.Value(visible ? 1 : 0), []);
// FUNCTIONALITY: Animation spring khi visible thay đổi
useEffect(() => {
const animation = Animated.spring(animatedValue, {
toValue: visible ? 1 : 0,
useNativeDriver: true, // PERFORMANCE: Sử dụng native driver để tối ưu animation
tension: 300,
friction: 10,
});
animation.start();
// CLEANUP: Dọn dẹp animation khi component unmount
return () => {
animation.stop();
};
}, [visible, animatedValue]);
// UI/UX: Style cho image tick được tính toán một lần
const imageStyle = useMemo(() => ({
width: size,
height: size ,
tintColor: R.colors.white,
backgroundColor:R.colors.black,
borderRadius:5
}), [size, color]);
return (
<Animated.View
style={{
transform: [{ scale: animatedValue }],
opacity: animatedValue,
justifyContent: 'center',
alignItems: 'center',
}}
>
<Image
source={IconTick}
style={imageStyle}
resizeMode="contain"
/>
</Animated.View>
);
});
// COMPONENT: Checkbox - Main component theo cấu trúc Button mới
const Checkbox = ({
// STATE: Checkbox value controls
value, // Controlled component value
defaultValue = false, // Uncontrolled component initial value
isCheck = false, // FEATURE: Mặc định bật tick ngay khi component mount
onValueChange,
// FUNCTIONALITY: Custom logic handlers
onPress, // FEATURE: Custom onPress handler để thực thi logic riêng
preventToggleOnPress = false, // FEATURE: Ngăn toggle tự động khi có custom onPress
// UI/UX: Checkbox appearance
size = 30,
borderRadius = 4,
borderColor = R.colors.black,
borderWidth = 1,
checkedColor = R.colors.main,
uncheckedColor = 'transparent',
tickColor = R.colors.white,
// FUNCTIONALITY: Label configuration
label,
labelPosition = 'right', // 'left' | 'right'
labelStyle,
labelSize = 16, // FEATURE: Kích thước font cho label
labelColor, // FEATURE: Màu sắc label (override default)
labelWeight = 'normal', // FEATURE: Font weight cho label ('normal', 'bold', '100', '200', ... '900')
labelOpacity, // FEATURE: Độ trong suốt label (override default)
labelSpacing = 5, // Khoảng cách giữa checkbox và label
labelNumberOfLines = 0, // FEATURE: Số dòng tối đa cho label (0 = unlimited)
labelEllipsizeMode = 'tail', // FEATURE: Cách cắt text khi quá dài ('head', 'middle', 'tail', 'clip')
labelLineHeight, // FEATURE: Chiều cao dòng cho label
labelLetterSpacing, // FEATURE: Khoảng cách giữa các ký tự
labelTextAlign = 'left', // FEATURE: Căn chỉnh text ('left', 'center', 'right')
labelTextDecorationLine, // FEATURE: Gạch chân/gạch ngang ('none', 'underline', 'line-through', 'underline line-through')
labelTextTransform, // FEATURE: Chuyển đổi chữ ('none', 'uppercase', 'lowercase', 'capitalize')
labelFontFamily, // FEATURE: Font family cho label
labelFontStyle = 'normal', // FEATURE: Font style ('normal', 'italic')
labelIncludeFontPadding = true, // FEATURE: Bao gồm font padding (Android only)
labelTextAlignVertical = 'auto', // FEATURE: Căn chỉnh vertical ('auto', 'top', 'bottom', 'center') - Android only
// UI/UX: Margin configuration
margin, // FEATURE: Margin cho toàn bộ component
marginTop, // FEATURE: Margin top riêng biệt
marginBottom, // FEATURE: Margin bottom riêng biệt
marginLeft, // FEATURE: Margin left riêng biệt
marginRight, // FEATURE: Margin right riêng biệt
marginHorizontal, // FEATURE: Margin horizontal (left + right)
marginVertical, // FEATURE: Margin vertical (top + bottom)
// STATE: Component state
disabled = false,
style,
testID,
}) => {
// STATE: Internal state cho uncontrolled component
// FEATURE: Ưu tiên isCheck > defaultValue để tự động bật tick
const [internalValue, setInternalValue] = useState(isCheck || defaultValue);
// FUNCTIONALITY: Xác định component là controlled hay uncontrolled
const isControlled = value !== undefined;
const currentValue = isControlled ? value : internalValue;
// FEATURE: Effect để xử lý isCheck khi component mount hoặc isCheck thay đổi
useEffect(() => {
// Chỉ áp dụng cho uncontrolled component
if (!isControlled && isCheck !== internalValue) {
setInternalValue(isCheck);
// FUNCTIONALITY: Trigger callback nếu có khi isCheck thay đổi
if (onValueChange && isCheck !== internalValue) {
onValueChange(isCheck);
}
}
}, [isCheck, isControlled, internalValue, onValueChange]);
// FUNCTIONALITY: Xử lý toggle checkbox - Đơn giản hóa logic
const handleToggle = useCallback(() => {
if (disabled) {
return;
}
const newValue = !currentValue;
// STATE: Cập nhật internal state nếu uncontrolled
if (!isControlled) {
setInternalValue(newValue);
}
// FUNCTIONALITY: Gọi callback nếu có - Wrap trong requestAnimationFrame để tránh conflict
if (onValueChange) {
requestAnimationFrame(() => {
onValueChange(newValue);
});
}
}, [currentValue, disabled, isControlled, onValueChange]);
// FUNCTIONALITY: Xử lý press event - Tối ưu để tránh multiple calls
const handlePress = useCallback(() => {
if (disabled) {
return;
}
// PERFORMANCE: Debounce để tránh multiple rapid taps
const now = Date.now();
if (handlePress.lastCall && (now - handlePress.lastCall) < 100) {
return;
}
handlePress.lastCall = now;
// FEATURE: Nếu có custom onPress handler
if (onPress) {
const checkboxInfo = {
currentValue,
isControlled,
toggleCheckbox: handleToggle,
};
try {
// FUNCTIONALITY: Thực thi custom logic
onPress(checkboxInfo);
// FUNCTIONALITY: Chỉ auto-toggle khi không bị prevent
if (!preventToggleOnPress) {
handleToggle();
}
} catch (error) {
console.error('Checkbox onPress error:', error);
}
return;
}
// FUNCTIONALITY: Không có custom onPress, toggle bình thường
handleToggle();
}, [
disabled,
onPress,
preventToggleOnPress,
currentValue,
isControlled,
handleToggle
]);
// UI/UX: Tính toán margin styles với priority
const marginStyles = useMemo(() => {
const styles = {};
// FEATURE: Áp dụng margin chung trước
if (margin !== undefined) {
styles.margin = margin;
}
// FEATURE: Áp dụng marginHorizontal và marginVertical
if (marginHorizontal !== undefined) {
styles.marginHorizontal = marginHorizontal;
}
if (marginVertical !== undefined) {
styles.marginVertical = marginVertical;
}
// FEATURE: Áp dụng margin riêng biệt (có priority cao nhất)
if (marginTop !== undefined) {
styles.marginTop = marginTop;
}
if (marginBottom !== undefined) {
styles.marginBottom = marginBottom;
}
if (marginLeft !== undefined) {
styles.marginLeft = marginLeft;
}
if (marginRight !== undefined) {
styles.marginRight = marginRight;
}
return styles;
}, [margin, marginTop, marginBottom, marginLeft, marginRight, marginHorizontal, marginVertical]);
// UI/UX: Style cho checkbox được memoize để tối ưu performance
const checkboxStyle = useMemo(() => ({
width: size,
height: size,
borderRadius,
borderWidth,
borderColor: currentValue ? checkedColor : borderColor,
backgroundColor: currentValue ? checkedColor : uncheckedColor,
opacity: disabled ? 0.5 : 1,
justifyContent: 'center',
alignItems: 'center',
}), [
size,
borderRadius,
borderWidth,
borderColor,
currentValue,
checkedColor,
uncheckedColor,
disabled,
]);
// UI/UX: Style cho label với các thuộc tính mở rộng
const defaultLabelStyle = useMemo(() => {
const baseStyle = {
fontSize: labelSize,
fontWeight: labelWeight,
fontStyle: labelFontStyle,
textAlign: labelTextAlign,
includeFontPadding: labelIncludeFontPadding,
textAlignVertical: labelTextAlignVertical,
};
// FEATURE: Màu sắc label với logic fallback
if (labelColor) {
baseStyle.color = labelColor;
} else {
baseStyle.color = disabled ? R.colors.gray : R.colors.black;
}
// FEATURE: Opacity label với logic fallback
if (labelOpacity !== undefined) {
baseStyle.opacity = labelOpacity;
} else {
baseStyle.opacity = disabled ? 0.5 : 1;
}
// FEATURE: Các thuộc tính typography tùy chọn
if (labelLineHeight) {
baseStyle.lineHeight = labelLineHeight;
}
if (labelLetterSpacing) {
baseStyle.letterSpacing = labelLetterSpacing;
}
if (labelTextDecorationLine) {
baseStyle.textDecorationLine = labelTextDecorationLine;
}
if (labelTextTransform) {
baseStyle.textTransform = labelTextTransform;
}
if (labelFontFamily) {
baseStyle.fontFamily = labelFontFamily;
}
return baseStyle;
}, [
labelSize,
labelColor,
labelWeight,
labelOpacity,
labelLineHeight,
labelLetterSpacing,
labelTextAlign,
labelTextDecorationLine,
labelTextTransform,
labelFontFamily,
labelFontStyle,
labelIncludeFontPadding,
labelTextAlignVertical,
disabled
]);
// UI/UX: Style cho container chính với margin
const containerStyle = useMemo(() => ({
flexDirection: labelPosition === 'right' ? 'row' : 'row-reverse',
alignItems: 'center',
...marginStyles, // FEATURE: Áp dụng margin styles
}), [labelPosition, marginStyles]);
// UI/UX: Style cho spacing giữa checkbox và label
const spacingStyle = useMemo(() => {
if (!label) return {};
return labelPosition === 'right'
? { marginLeft: labelSpacing }
: { marginRight: labelSpacing };
}, [label, labelPosition, labelSpacing]);
// FUNCTIONALITY: Render checkbox element
const renderCheckbox = () => (
<View style={checkboxStyle}>
{currentValue && (
<TickIcon
visible={currentValue}
size={size}
color={tickColor}
/>
)}
</View>
);
// FUNCTIONALITY: Render label element với các thuộc tính mở rộng
const renderLabel = () => {
if (!label) return null;
return (
<Text
style={[defaultLabelStyle, labelStyle, spacingStyle]}
numberOfLines={labelNumberOfLines}
ellipsizeMode={labelEllipsizeMode}
allowFontScaling={true} // FEATURE: Cho phép scale font theo system settings
adjustsFontSizeToFit={false} // FEATURE: Không auto-adjust font size
minimumFontScale={0.01} // FEATURE: Minimum scale factor khi adjustsFontSizeToFit = true
suppressHighlighting={true} // FEATURE: Ngăn highlight khi press (iOS)
selectable={false} // FEATURE: Không cho phép select text
textBreakStrategy="simple" // FEATURE: Text break strategy (Android)
>
{label}
</Text>
);
};
// UI/UX: Nếu không có label, render checkbox đơn giản với margin
if (!label) {
return (
<TouchableOpacity
style={[checkboxStyle, marginStyles, style]} // FEATURE: Áp dụng marginStyles
onPress={handlePress} // FEATURE: Sử dụng handlePress thay vì handleToggle
disabled={disabled}
activeOpacity={0.8}
testID={testID}
accessible={true}
accessibilityRole="checkbox"
accessibilityState={{ checked: currentValue, disabled }}
accessibilityHint="Tap to toggle checkbox"
>
{currentValue && (
<TickIcon
visible={currentValue}
size={size}
color={tickColor}
/>
)}
</TouchableOpacity>
);
}
// UI/UX: Render checkbox với label và margin
return (
<TouchableOpacity
style={[containerStyle, style]} // marginStyles đã được merge vào containerStyle
onPress={handlePress} // FEATURE: Sử dụng handlePress thay vì handleToggle
disabled={disabled}
activeOpacity={0.8}
testID={testID}
accessible={true}
accessibilityRole="checkbox"
accessibilityState={{ checked: currentValue, disabled }}
accessibilityLabel={typeof label === 'string' ? label : 'Checkbox'}
accessibilityHint="Tap to toggle checkbox"
>
{renderCheckbox()}
{renderLabel()}
</TouchableOpacity>
);
};
export default React.memo(Checkbox);
\ No newline at end of file
......@@ -36,9 +36,35 @@ const DetailStudent = (props) => {
},
]
});
const handleCheckboxChange = (itemId, newValue) => {
setStudent(prevStudent => {
// Sao chép danh sách type cũ
const updatedTypes = prevStudent.type.map(item => {
if (item.id === itemId) {
// Nếu id trùng -> tạo object mới với status cập nhật
return {
...item,
status: newValue,
};
} else {
// Nếu không trùng -> giữ nguyên item
return item;
}
});
// Trả về student mới với type đã cập nhật
return {
...prevStudent,
type: updatedTypes,
};
});
};
return (
<DetailStudentView
student={student}
onCheckboxChange={handleCheckboxChange}
/>
);
};
......
......@@ -59,6 +59,22 @@ const styles = StyleSheet.create({
color:R.colors.blue,
marginHorizontal:15,
},
label:{
fontSize:R.sizes.sm,
fontFamily:R.fonts.fontMedium,
fontWeight:'600',
color:R.colors.black,
},
checkboxContainer: {
flexDirection: 'row',
flexWrap: 'wrap',
justifyContent: 'space-between',
},
checkboxItem: {
width: '50%',
marginBottom: 15,
},
})
export default styles
......@@ -12,8 +12,9 @@ import styles from './style';
import * as SCREENNAME from '../../../../routers/ScreenNames';
import {useNavigation} from '@react-navigation/native';
import VersionCheck from '../../../../components/VersionCheck';
import CheckBox from '../../../../components/CheckBox';
const DetailStudentView = props => {
const {student} = props;
const {student, onCheckboxChange} = props;
const navigate = useNavigation();
return (
<SafeAreaView style={styles.container}>
......@@ -116,6 +117,26 @@ const DetailStudentView = props => {
</Text>
</View>
</View>
{/*Row 6*/}
<View style={styles.checkboxContainer}>
{student.type.map((item, index) => (
<View key={item.id} style={styles.checkboxItem}>
<CheckBox
value={item.status}
onValueChange={value => onCheckboxChange(item.id, value)}
size={20}
labelStyle={styles.label}
checkedColor={R.colors.main}
tickColor={R.colors.white}
label={item.name}
imageStyle={styles.imageStyle}
/>
</View>
))}
</View>
</View>
<Text style={styles.textTitle}>Lch s tình trng ca sinh viên</Text>
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment