Commit e5a34df6 by tungnq

TODO: Đã hoàn thiện giao diện lịch theo tuần và theo tháng

parent 6e9a6de1
import { StyleSheet } from 'react-native'
import R from '../../../assets/R';
const styles = StyleSheet.create({
container:{
flex:1,
backgroundColor:R.colors.white
},
body:{
flex:1,
backgroundColor:R.colors.white
}
})
export default styles
import React from 'react'; import React, {useState, useMemo, useRef} from 'react';
import {Text, View, StyleSheet} from 'react-native'; import {Animated, PanResponder, Dimensions} from 'react-native';
import ClassScheduleView from './view'; import ClassScheduleView from './view';
const ClassSchedule = (props) => { const {height: screenHeight} = Dimensions.get('window');
const monthNames = [ const BOTTOM_SHEET_HEIGHT = screenHeight * 0.6;
'Tháng 1', 'Tháng 2', 'Tháng 3', 'Tháng 4', 'Tháng 5', 'Tháng 6',
'Tháng 7', 'Tháng 8', 'Tháng 9', 'Tháng 10', 'Tháng 11', 'Tháng 12', const ClassSchedule = ({events = [], onDateSelect, onEventPress}) => {
const [currentDate, setCurrentDate] = useState(new Date(2025, 7, 1));
const [selectedDate, setSelectedDate] = useState(null);
const [showBottomSheet, setShowBottomSheet] = useState(false);
const bottomSheetTranslateY = useRef(new Animated.Value(BOTTOM_SHEET_HEIGHT)).current;
const formatDateToString = (date) => {
const year = date.getFullYear();
const month = (date.getMonth() + 1).toString().padStart(2, '0');
const day = date.getDate().toString().padStart(2, '0');
return `${year}-${month}-${day}`;
};
const createMockEvents = () => {
const today = new Date();
const todayStr = formatDateToString(today);
const tomorrow = new Date(today);
tomorrow.setDate(tomorrow.getDate() + 1);
const tomorrowStr = formatDateToString(tomorrow);
return [
{
id: '1',
title: 'Meeting hôm nay',
date: todayStr,
time: '10:00',
endTime: '11:00',
description: 'Họp team development',
type: 'meeting',
},
{
id: '2',
title: 'Demo hôm nay',
date: todayStr,
time: '15:00',
endTime: '16:00',
description: 'Present tính năng mới',
type: 'demo',
},
{
id: '11',
title: 'Lịch học lớp IT47.8F7',
date: '2025-08-04',
time: '07:00',
endTime: '08:30',
description: 'Môn học chuyên ngành',
type: 'class',
},
{
id: '12',
title: 'Meeting team',
date: '2025-08-04',
time: '10:00',
endTime: '11:00',
description: 'Họp team development',
type: 'meeting',
},
{
id: '13',
title: 'Training React Native',
date: '2025-08-05',
time: '14:00',
endTime: '16:00',
description: 'Học New Architecture',
type: 'training',
},
{
id: '14',
title: 'Code Review',
date: '2025-08-05',
time: '10:30',
endTime: '11:30',
description: 'Review PR #123',
type: 'review',
},
{
id: '15',
title: 'Họp nội bộ giữa giảng viên bộ môn công nghệ phần mềm',
date: '2025-08-05',
time: '09:00',
endTime: '11:30',
description: 'Thảo luận chương trình đào tạo mới',
type: 'meeting',
},
{
id: '16',
title: 'Sự kiện thắp sáng ước mơ kỹ thuật',
date: '2025-08-05',
time: '13:00',
endTime: '15:30',
description: 'Chương trình định hướng nghề nghiệp cho sinh viên',
type: 'event',
},
{
id: '17',
title: 'Lịch học lớp EWC45.364.L1',
date: '2025-08-05',
time: '14:00',
endTime: '15:30',
description: 'Tiếng Anh chuyên ngành',
type: 'class',
},
{
id: '18',
title: 'Họp tổng kết quả chấm nghiệm cứu khoa học sinh viên khóa K18',
date: '2025-08-05',
time: '17:00',
endTime: '20:30',
description: 'Đánh giá kết quả nghiên cứu khoa học của sinh viên',
type: 'meeting',
},
{
id: '3',
title: 'Training React Native',
date: tomorrowStr,
time: '14:00',
endTime: '16:00',
description: 'Học New Architecture',
type: 'training',
},
{
id: '4',
title: 'Code Review',
date: tomorrowStr,
time: '10:30',
endTime: '11:30',
description: 'Review PR #123',
type: 'review',
},
{
id: '10',
title: 'Demo sản phẩm',
date: '2025-08-25',
time: '15:00',
endTime: '16:30',
description: 'Present tính năng mới',
type: 'demo',
},
]; ];
};
const mockEvents = createMockEvents();
const allEvents = [...events, ...mockEvents];
const panResponder = useRef(
PanResponder.create({
onMoveShouldSetPanResponder: (evt, gestureState) => {
return Math.abs(gestureState.dy) > 10;
},
onPanResponderMove: (evt, gestureState) => {
if (gestureState.dy > 0) {
bottomSheetTranslateY.setValue(gestureState.dy);
}
},
onPanResponderRelease: (evt, gestureState) => {
if (gestureState.dy > 100) {
hideBottomSheetModal();
} else {
Animated.spring(bottomSheetTranslateY, {
toValue: 0,
useNativeDriver: true,
}).start();
}
},
})
).current;
const getMonthData = useMemo(() => {
const year = currentDate.getFullYear();
const month = currentDate.getMonth();
const firstDay = new Date(year, month, 1);
const lastDay = new Date(year, month + 1, 0);
const startDate = new Date(firstDay);
startDate.setDate(firstDay.getDate() - firstDay.getDay());
const days = [];
const currentDateObj = new Date(startDate);
for (let i = 0; i < 42; i++) {
days.push(new Date(currentDateObj));
currentDateObj.setDate(currentDateObj.getDate() + 1);
}
return {
year,
month,
firstDay,
lastDay,
days,
};
}, [currentDate]);
const getEventsForDate = (date) => {
const dateStr = formatDateToString(date);
return allEvents.filter(event => event.date === dateStr);
};
const parseLocalDate = (dateString) => {
const [year, month, day] = dateString.split('-').map(Number);
return new Date(year, month - 1, day);
};
const formatDateToDisplay = (date) => {
const day = date.getDate().toString().padStart(2, '0');
const month = (date.getMonth() + 1).toString().padStart(2, '0');
const year = date.getFullYear();
return `Lịch ngày ${day}/${month}/${year}`;
};
const isCurrentMonth = (date) => {
return date.getMonth() === currentDate.getMonth();
};
const isToday = (date) => {
const today = new Date();
return (
date.getDate() === today.getDate() &&
date.getMonth() === today.getMonth() &&
date.getFullYear() === today.getFullYear()
);
};
const navigateMonth = (direction) => {
const newDate = new Date(currentDate);
if (direction === 'prev') {
newDate.setMonth(newDate.getMonth() - 1);
} else {
newDate.setMonth(newDate.getMonth() + 1);
}
setCurrentDate(newDate);
setSelectedDate(null);
if (showBottomSheet) {
hideBottomSheetModal();
}
};
const showBottomSheetModal = () => {
setShowBottomSheet(true);
Animated.spring(bottomSheetTranslateY, {
toValue: 0,
tension: 100,
friction: 8,
useNativeDriver: true,
}).start();
};
const hideBottomSheetModal = () => {
Animated.timing(bottomSheetTranslateY, {
toValue: BOTTOM_SHEET_HEIGHT,
duration: 300,
useNativeDriver: true,
}).start(() => {
setShowBottomSheet(false);
});
};
const handleDatePress = (date) => {
const dateStr = formatDateToString(date);
const dayEvents = getEventsForDate(date);
setSelectedDate(dateStr);
onDateSelect?.(dateStr);
if (dayEvents.length > 0) {
showBottomSheetModal();
}
};
const handleEventPress = (event) => {
onEventPress?.(event);
};
const handleCloseBottomSheet = () => {
hideBottomSheetModal();
};
const getSelectedEvents = () => {
if (!selectedDate) return [];
return allEvents
.filter(event => event.date === selectedDate)
.sort((a, b) => a.time.localeCompare(b.time));
};
return ( return (
<ClassScheduleView monthNames={monthNames}/> <ClassScheduleView
currentDate={currentDate}
selectedDate={selectedDate}
showBottomSheet={showBottomSheet}
bottomSheetTranslateY={bottomSheetTranslateY}
panResponder={panResponder}
getMonthData={getMonthData}
getEventsForDate={getEventsForDate}
parseLocalDate={parseLocalDate}
formatDateToDisplay={formatDateToDisplay}
isCurrentMonth={isCurrentMonth}
isToday={isToday}
navigateMonth={navigateMonth}
handleDatePress={handleDatePress}
handleEventPress={handleEventPress}
handleCloseBottomSheet={handleCloseBottomSheet}
getSelectedEvents={getSelectedEvents}
/>
); );
}; };
......
import { StyleSheet } from 'react-native' import {StyleSheet, Dimensions} from 'react-native';
import R from '../../assets/R'; import R from '../../assets/R';
const {width: screenWidth, height: screenHeight} = Dimensions.get('window');
const CELL_WIDTH = (screenWidth - 30) / 7;
const CELL_HEIGHT = (screenHeight - 140) / 6;
const BOTTOM_SHEET_HEIGHT = screenHeight * 0.6;
const styles = StyleSheet.create({ const styles = StyleSheet.create({
container:{ container: {
flex:1, flex: 1,
backgroundColor:R.colors.white backgroundColor: R.colors.white,
alignItems: 'center',
},
header: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
paddingVertical: 15,
},
header_title: {
fontSize: R.fontsize.fontsSizeTitle,
fontFamily: R.fonts.fontMedium,
color: R.colors.black,
fontWeight: '600',
},
navButton: {
width: 30,
height: 30,
borderRadius: 20,
backgroundColor: R.colors.blue,
alignItems: 'center',
justifyContent: 'center',
},
navButtonText: {
color: R.colors.white,
fontSize: R.fontsize.fontsSizeTitle,
fontFamily: R.fonts.fontMedium,
},
weekDaysContainer: {
flexDirection: 'row',
paddingBottom: 5,
marginBottom: 5,
},
weekDayCell: {
width: CELL_WIDTH,
alignItems: 'center',
},
weekDayText: {
fontFamily: R.fonts.fontRegular,
fontSize: R.fontsize.fontSizeLabel,
fontWeight: '400',
color: R.colors.black,
},
calendarGrid: {
},
weekRow: {
flexDirection: 'row',
},
dayCell: {
width: CELL_WIDTH,
minHeight: CELL_HEIGHT,
borderWidth: 1,
borderColor: R.colors.grayBorderInputTextHeader,
padding: 4,
alignItems: 'center',
},
selectedDayCell: {
borderColor: R.colors.blue,
borderWidth: 1,
},
dayText: {
fontSize: R.fontsize.fontSizeLabel,
fontWeight: '500',
fontFamily:R.fonts.fontSemiBold,
color: R.colors.black,
marginBottom: 2,
},
dayTextInactive: {
color: R.colors.black,
opacity: 1,
},
selectedDayText: {
color: R.colors.black,
fontWeight: 'bold',
fontFamily: R.fonts.fontSemiBold,
},
todayText: {
color: R.colors.white,
fontWeight: 'bold',
fontFamily: R.fonts.fontSemiBold,
backgroundColor: R.colors.blue,
borderRadius:15,
paddingHorizontal: 4,
paddingVertical: 3,
},
eventsContainer: {
width: '100%',
flex: 1,
},
eventBar: {
paddingVertical: 2,
paddingHorizontal: 5,
borderRadius: 10,
marginBottom: 2,
backgroundColor:R.colors.blue
},
eventBarText: {
fontSize: R.fontsize.fontSizeLabel,
color: R.colors.white,
fontWeight: '500',
fontFamily: R.fonts.fontRegular
},
moreEventsText: {
fontSize: R.sizes.xs,
color: R.colors.grey_100,
textAlign: 'center',
},
modalBackdrop: {
flex: 1,
backgroundColor: R.colors.grey_200,
justifyContent: 'flex-end',
},
bottomSheet: {
height: BOTTOM_SHEET_HEIGHT,
backgroundColor: R.colors.white,
borderTopLeftRadius: 20,
borderTopRightRadius: 20,
},
bottomSheetContent: {
paddingHorizontal: 15,
},
dragHandle: {
width: 40,
height: 4,
backgroundColor: R.colors.grey_200,
borderRadius: 2,
alignSelf: 'center',
marginTop: 10,
marginBottom: 15,
},
bottomSheetHeader: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
marginBottom: 20,
},
bottomSheetTitle: {
fontSize: R.fontsize.fontSizeHeader1,
fontFamily: R.fonts.InterMedium,
color: R.colors.black,
flex: 1,
},
closeButton: {
width: 30,
height: 30,
borderRadius: 15,
backgroundColor: R.colors.grey_200,
alignItems: 'center',
justifyContent: 'center',
},
closeButtonText: {
fontSize: R.fontsize.fontsSize12,
color: R.colors.grey_800,
fontFamily: R.fonts.InterRegular,
},
noEventsContainer: {
flex: 1,
alignItems: 'center',
justifyContent: 'center',
paddingVertical: 40,
},
noEventsText: {
fontSize: R.fontsize.fontsSize12,
fontFamily: R.fonts.InterRegular,
color: R.colors.grey_800,
fontWeight: '400'
},
eventCard: {
flexDirection: 'row',
backgroundColor: R.colors.white,
borderRadius: 12,
padding: 15,
marginBottom: 12,
borderLeftWidth: 4,
borderLeftColor: R.colors.blue500,
shadowColor: R.colors.black,
shadowOffset: {
width: 0,
height: 2,
},
shadowOpacity: 1,
shadowRadius: 1,
elevation: 2,
},
eventTimeContainer: {
minWidth: 80,
alignItems: 'flex-start',
justifyContent: 'flex-start',
marginRight: 15,
},
eventTime: {
fontSize: R.fontsize.fontsSize12,
fontFamily: R.fonts.InterMedium,
color: R.colors.blue500,
fontWeight: '600',
},
eventContent: {
flex: 1,
},
eventTitle: {
fontSize: R.fontsize.fontsSize12,
fontFamily: R.fonts.InterMedium,
color: R.colors.black,
fontWeight: '600',
marginBottom: 4,
},
eventDescription: {
fontSize: R.fontsize.fontsSize12,
fontFamily: R.fonts.InterRegular,
color: R.colors.grey_800,
}, },
body:{ });
flex:1,
backgroundColor:R.colors.white
}
})
export default styles export {styles, CELL_WIDTH, BOTTOM_SHEET_HEIGHT};
\ No newline at end of file
import React from 'react'; import React from 'react';
import {Text, View, TouchableOpacity, StyleSheet} from 'react-native'; import {
import styles from './style'; Text,
const ClassScheduleView = (props) => { View,
const { } = props; TouchableOpacity,
const renderHeader =()=>{ StyleSheet,
return( ScrollView,
<View> Dimensions,
Modal,
Animated,
SafeAreaView,
} from 'react-native';
import R from '../../assets/R';
import { styles, CELL_WIDTH, BOTTOM_SHEET_HEIGHT } from './style';
import { useNavigation } from '@react-navigation/native';
import * as SCREENNAME from '../../routers/ScreenNames';
const ClassScheduleView = ({
currentDate,
selectedDate,
showBottomSheet,
bottomSheetTranslateY,
panResponder,
getMonthData,
getEventsForDate,
parseLocalDate,
formatDateToDisplay,
isCurrentMonth,
isToday,
navigateMonth,
handleDatePress,
handleEventPress,
handleCloseBottomSheet,
getSelectedEvents,
}) => {
const navigation = useNavigation();
const renderHeader = () => {
const monthNames = [
'Tháng 1', 'Tháng 2', 'Tháng 3', 'Tháng 4', 'Tháng 5', 'Tháng 6',
'Tháng 7', 'Tháng 8', 'Tháng 9', 'Tháng 10', 'Tháng 11', 'Tháng 12',
];
return (
<View style={styles.header}>
<TouchableOpacity
style={styles.navButton}
onPress={() => navigateMonth('prev')}>
<Text style={styles.navButtonText}></Text>
</TouchableOpacity>
<Text style={styles.header_title}>
{monthNames[getMonthData.month]} {getMonthData.year}
</Text>
<TouchableOpacity
style={styles.navButton}
onPress={() => navigateMonth('next')}>
<Text style={styles.navButtonText}></Text>
</TouchableOpacity>
</View> </View>
) );
};
const renderWeekDays = () => {
const weekDays = ['CN', 'T2', 'T3', 'T4', 'T5', 'T6', 'T7'];
return (
<View style={styles.weekDaysContainer}>
{weekDays.map((day, index) => (
<View key={index} style={styles.weekDayCell}>
<Text style={styles.weekDayText}>{day}</Text>
</View>
))}
</View>
);
};
const renderDayCell = (date, index) => {
const dayEvents = getEventsForDate(date);
const isSelected = selectedDate === formatDateToString(date);
const isTodayDate = isToday(date);
const isInCurrentMonth = isCurrentMonth(date);
return (
<TouchableOpacity
key={index}
style={[
styles.dayCell,
isSelected && styles.selectedDayCell,
isTodayDate && styles.todayCell,
!isInCurrentMonth && {backgroundColor: R.colors.gray},
]}
onPress={() => handleDatePress(date)}
activeOpacity={0.7}>
<Text
style={[
styles.dayText,
!isInCurrentMonth && styles.dayTextInactive,
isSelected && styles.selectedDayText,
isTodayDate && styles.todayText,
]}>
{date.getDate()}
</Text>
{dayEvents.length > 0 && (
<View style={styles.eventsContainer}>
{dayEvents.slice(0, 2).map((event, eventIndex) => (
<TouchableOpacity
key={event.id}
style={[
styles.eventBar,
]}
onPress={() => handleEventPress(event)}>
<Text style={styles.eventBarText} numberOfLines={1}>
{event.title}
</Text>
</TouchableOpacity>
))}
{dayEvents.length > 2 && (
<Text style={styles.moreEventsText}>+{dayEvents.length - 2}</Text>
)}
</View>
)}
</TouchableOpacity>
);
};
const formatDateToString = (date) => {
const year = date.getFullYear();
const month = (date.getMonth() + 1).toString().padStart(2, '0');
const day = date.getDate().toString().padStart(2, '0');
return `${year}-${month}-${day}`;
};
const renderCalendarGrid = () => {
const weeks = [];
for (let i = 0; i < 6; i++) {
const week = getMonthData.days.slice(i * 7, (i + 1) * 7);
weeks.push(
<View key={i} style={styles.weekRow}>
{week.map((date, dayIndex) =>
renderDayCell(date, i * 7 + dayIndex),
)}
</View>,
);
} }
return <View style={styles.calendarGrid}>{weeks}</View>;
};
const renderBottomSheetContent = () => {
if (!selectedDate) return null;
const selectedDateObj = parseLocalDate(selectedDate);
const selectedEvents = getSelectedEvents();
return ( return (
<View <View style={styles.bottomSheetContent}>
style={styles.container}> <View style={styles.dragHandle} />
<View style={styles.body}>
<View style={styles.bottomSheetHeader}>
<Text style={styles.bottomSheetTitle}>
{formatDateToDisplay(selectedDateObj)}
</Text>
<TouchableOpacity
style={styles.closeButton}
onPress={handleCloseBottomSheet}>
<Text style={styles.closeButtonText}></Text>
</TouchableOpacity>
</View> </View>
<ScrollView
style={styles.eventsScrollView}
showsVerticalScrollIndicator={false}>
{selectedEvents.length === 0 ? (
<View style={styles.noEventsContainer}>
<Text style={styles.noEventsText}>Không có s kin nào</Text>
</View>
) : (
selectedEvents.map((event, index) => (
<TouchableOpacity
key={event.id}
style={styles.eventCard}
onPress={() => navigation.navigate(SCREENNAME.DETAILCLASSSCHEDULE, { event })}
activeOpacity={0.7}>
<View style={styles.eventTimeContainer}>
<Text style={styles.eventTime}>
{event.time}
{event.endTime && ` - ${event.endTime}`}
</Text>
</View>
<View style={styles.eventContent}>
<Text style={styles.eventTitle} numberOfLines={2}>
{event.title}
</Text>
{event.description && (
<Text style={styles.eventDescription} numberOfLines={3}>
{event.description}
</Text>
)}
</View>
</TouchableOpacity>
))
)}
</ScrollView>
</View> </View>
); );
};
const renderBottomSheet = () => {
return (
<Modal
visible={showBottomSheet}
transparent={true}
animationType="none"
onRequestClose={handleCloseBottomSheet}>
<TouchableOpacity
style={styles.modalBackdrop}
activeOpacity={1}
onPress={handleCloseBottomSheet}>
<Animated.View
style={[
styles.bottomSheet,
{
transform: [{ translateY: bottomSheetTranslateY }],
},
]}
{...panResponder.panHandlers}>
<TouchableOpacity activeOpacity={1}>
{renderBottomSheetContent()}
</TouchableOpacity>
</Animated.View>
</TouchableOpacity>
</Modal>
);
};
return (
<SafeAreaView style={styles.container}>
<ScrollView showsVerticalScrollIndicator={false}>
{renderHeader()}
{renderWeekDays()}
{renderCalendarGrid()}
</ScrollView>
{renderBottomSheet()}
</SafeAreaView>
);
}; };
export default ClassScheduleView; export default ClassScheduleView;
\ No newline at end of file
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