Commit e10510ea by tungnq

TODO: Đã chỉnh sửa xong filter tháng

parent ad6996b4
...@@ -7,7 +7,7 @@ ...@@ -7,7 +7,7 @@
*/ */
import React, {useEffect} from 'react'; import React, {useEffect} from 'react';
import {View, Text} from 'react-native'; import {View, Text, DeviceEventEmitter} from 'react-native';
import {Provider} from 'react-redux'; import {Provider} from 'react-redux';
import {createStore, applyMiddleware} from 'redux'; import {createStore, applyMiddleware} from 'redux';
import rootReducer from './src/reducers/index'; import rootReducer from './src/reducers/index';
...@@ -16,6 +16,15 @@ import createSagaMiddleware from 'redux-saga'; ...@@ -16,6 +16,15 @@ import createSagaMiddleware from 'redux-saga';
import rootSaga from './src/saga/rootSaga'; import rootSaga from './src/saga/rootSaga';
// import FirebaseNotification from "./src/helper/FirebaseNotification"; // import FirebaseNotification from "./src/helper/FirebaseNotification";
import SplashScreen from 'react-native-splash-screen'; import SplashScreen from 'react-native-splash-screen';
if (!DeviceEventEmitter.removeListener) {
DeviceEventEmitter.removeListener = function(eventType, listener) {
// Use the new API
const subscription = this.addListener(eventType, () => {});
if (subscription && subscription.remove) {
subscription.remove();
}
};
}
const sagaMiddleware = createSagaMiddleware(); const sagaMiddleware = createSagaMiddleware();
let store = createStore(rootReducer, applyMiddleware(sagaMiddleware)); let store = createStore(rootReducer, applyMiddleware(sagaMiddleware));
......
...@@ -43,6 +43,8 @@ const images = { ...@@ -43,6 +43,8 @@ const images = {
icProfileUn:require('./icon/profile_navigation_unsel.png'), icProfileUn:require('./icon/profile_navigation_unsel.png'),
igProfileDemo:require('./images/image.png'), igProfileDemo:require('./images/image.png'),
icImageDownload:require('./images/icImageDownload.png'), icImageDownload:require('./images/icImageDownload.png'),
icLeft:require('./icon/arrow_left.png'),
icRight:require('./icon/arrow_right.png'),
ic3Date:Date3Drawer, ic3Date:Date3Drawer,
icDateDrawer:DateDrawer, icDateDrawer:DateDrawer,
icMenu:Menu, icMenu:Menu,
......
import React from 'react'; import React, { useRef, useEffect } from 'react';
import { import {
Dimensions, Dimensions,
Platform, Platform,
...@@ -234,6 +234,9 @@ export const removeItemFromArr2 = (items, index) => { ...@@ -234,6 +234,9 @@ export const removeItemFromArr2 = (items, index) => {
return fill; return fill;
}; };
export const isValidEmail = email =>
email.length > 0 && /(.+)@(.+){2,}\.(.+){2,}/gim.test(email);
export const removeItemFromArr = (items, index) => { export const removeItemFromArr = (items, index) => {
items.splice(index, 1); items.splice(index, 1);
return items; return items;
...@@ -601,3 +604,138 @@ export const getMimeType = fileExt => { ...@@ -601,3 +604,138 @@ export const getMimeType = fileExt => {
return '*/*'; return '*/*';
} }
}; };
//Calendar
export const parseMinutes = timeStr => {
if (!timeStr || typeof timeStr !== 'string') return 0;
const parts = timeStr.split(':');
if (parts.length !== 2) return 0;
const [h, m] = parts.map(Number);
if (isNaN(h) || isNaN(m)) return 0;
return h * 60 + m;
};
export const formatDateToString = date => {
const y = date.getFullYear();
const m = String(date.getMonth() + 1).padStart(2, '0');
const d = String(date.getDate()).padStart(2, '0');
return `${y}-${m}-${d}`;
};
export const isSameDay = (a, b) =>
a.getFullYear() === b.getFullYear() &&
a.getMonth() === b.getMonth() &&
a.getDate() === b.getDate();
/**
* Trả về mảng events đã có toạ độ:
* { topPosition, height, leftOffset, rightOffset, zIndex, columnIndex, numColumns }
*/
export const layoutDayEvents = (events, hourHeight = 80) => {
if (!events || events.length === 0) return [];
// Chuẩn hoá + sort theo start
const mapped = events
.filter(e => e?.time && e?.endTime)
.map((e, i) => ({
...e,
start: parseMinutes(e.time),
end: parseMinutes(e.endTime),
_idx: i,
}))
.filter(e => e.end > e.start)
.sort((a, b) => a.start - b.start);
// Gom nhóm overlap tuyến tính
const groups = [];
let cur = null;
for (const ev of mapped) {
if (!cur || ev.start >= cur.maxEnd) {
cur = {id: groups.length, items: [ev], maxEnd: ev.end};
groups.push(cur);
} else {
cur.items.push(ev);
if (ev.end > cur.maxEnd) cur.maxEnd = ev.end;
}
}
const out = [];
// Gán cột theo greedy interval partitioning
for (const g of groups) {
const colEnds = []; // end time cuối mỗi cột
const colOf = {}; // event.id -> columnIndex
const items = [...g.items].sort((a, b) => a.start - b.start);
for (const ev of items) {
let placed = false;
for (let c = 0; c < colEnds.length; c++) {
if (ev.start >= colEnds[c]) {
colEnds[c] = ev.end;
colOf[ev.id] = c;
placed = true;
break;
}
}
if (!placed) {
colOf[ev.id] = colEnds.length;
colEnds.push(ev.end);
}
}
const numCols = Math.max(colEnds.length, 1);
for (const ev of items) {
const columnIndex = colOf[ev.id] ?? 0;
const topPosition = (ev.start / 60) * hourHeight;
const height = ((ev.end - ev.start) / 60) * hourHeight;
const widthPct = 100 / numCols;
const leftPct = columnIndex * widthPct;
out.push({
...ev,
topPosition,
height,
leftOffset: `${Math.round(leftPct * 100) / 100}%`,
rightOffset: `${Math.round((100 - (leftPct + widthPct)) * 100) / 100}%`,
widthPct,
columnIndex,
numColumns: numCols,
zIndex: 10 + columnIndex,
});
}
}
// Trả về theo thứ tự ban đầu để render ổn định
return out.sort((a, b) => a._idx - b._idx);
/**
* Hook đếm số lần render của một component
*/
};
export const useRenderCount = (name = 'Component') => {
const renderCount = useRef(0);
renderCount.current += 1;
useEffect(() => {
console.log(`${name} đã render ${renderCount.current} lần`);
});
return renderCount.current;
};
export 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',
]
\ No newline at end of file
...@@ -124,8 +124,8 @@ export default connect(mapStateToProps, {})(TabNavigator); ...@@ -124,8 +124,8 @@ export default connect(mapStateToProps, {})(TabNavigator);
const styles = StyleSheet.create({ const styles = StyleSheet.create({
tabBarIcon: { tabBarIcon: {
width: Platform.OS === 'ios' ? 20: '50%', width: Platform.OS === 'ios' ? 20: 25,
height: Platform.OS === 'ios' ? 20 : '50%', height: Platform.OS === 'ios' ? 20 : 25,
resizeMode: 'contain' resizeMode: 'contain'
} }
}); });
\ No newline at end of file
import React, {useState, useMemo, useRef} from 'react'; import React, {useState, useMemo, useRef, useEffect} from 'react';
import {Animated, PanResponder, Dimensions} from 'react-native'; import {Animated, PanResponder, Dimensions, DeviceEventEmitter} from 'react-native';
import {useFocusEffect} from '@react-navigation/native';
import ClassScheduleView from './view'; import ClassScheduleView from './view';
// ==================== HẰNG SỐ ====================
const {height: screenHeight} = Dimensions.get('window'); const {height: screenHeight} = Dimensions.get('window');
const BOTTOM_SHEET_HEIGHT = screenHeight * 0.6; const BOTTOM_SHEET_HEIGHT = screenHeight * 0.6;
const ClassSchedule = ({events = [], onDateSelect, onEventPress}) => { const ClassSchedule = ({events = [], onDateSelect, onEventPress}) => {
const [currentDate, setCurrentDate] = useState(new Date(2025, 7, 1)); // ==================== QUẢN LÝ STATE ====================
// B1: State ngày tháng và lịch
const [currentDate, setCurrentDate] = useState(new Date());
const [selectedDate, setSelectedDate] = useState(null); const [selectedDate, setSelectedDate] = useState(null);
// ==================== EFFECTS ====================
// Reset về ngày hiện tại khi chuyển màn hình
useFocusEffect(
React.useCallback(() => {
const today = new Date();
setCurrentDate(today);
setSelectedDate(null);
DeviceEventEmitter.emit('onDateChange', today);
// Cập nhật header drawer với tháng hiện tại
DeviceEventEmitter.emit('updateHeaderMonth', today.getMonth());
}, [])
);
// B2: State bottom sheet
const [showBottomSheet, setShowBottomSheet] = useState(false); const [showBottomSheet, setShowBottomSheet] = useState(false);
const bottomSheetTranslateY = useRef(new Animated.Value(BOTTOM_SHEET_HEIGHT)).current;
const formatDateToString = (date) => { // B3: Tham chiếu animation
const bottomSheetTranslateY = useRef(
new Animated.Value(BOTTOM_SHEET_HEIGHT),
).current;
// ==================== HÀM TIỆN ÍCH ====================
// T1: Định dạng ngày thành chuỗi
const formatDateToString = date => {
const year = date.getFullYear(); const year = date.getFullYear();
const month = (date.getMonth() + 1).toString().padStart(2, '0'); const month = (date.getMonth() + 1).toString().padStart(2, '0');
const day = date.getDate().toString().padStart(2, '0'); const day = date.getDate().toString().padStart(2, '0');
return `${year}-${month}-${day}`; return `${year}-${month}-${day}`;
}; };
// T2: Định dạng ngày để hiển thị
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}`;
};
// T3: Chuyển đổi chuỗi thành ngày
const parseLocalDate = dateString => {
const [year, month, day] = dateString.split('-').map(Number);
return new Date(year, month - 1, day);
};
// ==================== QUẢN LÝ DỮ LIỆU ====================
// D1: Tạo dữ liệu sự kiện mẫu
const createMockEvents = () => { const createMockEvents = () => {
const today = new Date(); const today = new Date();
const todayStr = formatDateToString(today); const todayStr = formatDateToString(today);
...@@ -147,32 +187,25 @@ const ClassSchedule = ({events = [], onDateSelect, onEventPress}) => { ...@@ -147,32 +187,25 @@ const ClassSchedule = ({events = [], onDateSelect, onEventPress}) => {
]; ];
}; };
// D2: Xử lý dữ liệu sự kiện
const mockEvents = createMockEvents(); const mockEvents = createMockEvents();
const allEvents = [...events, ...mockEvents]; const allEvents = [...events, ...mockEvents];
const panResponder = useRef( // D3: Hàm truy vấn sự kiện
PanResponder.create({ const getEventsForDate = date => {
onMoveShouldSetPanResponder: (evt, gestureState) => { const dateStr = formatDateToString(date);
return Math.abs(gestureState.dy) > 10; return allEvents.filter(event => event.date === dateStr);
}, };
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 getSelectedEvents = () => {
if (!selectedDate) return [];
return allEvents
.filter(event => event.date === selectedDate)
.sort((a, b) => a.time.localeCompare(b.time));
};
// ==================== LOGIC LỊCH ====================
// L1: Tạo dữ liệu tháng
const getMonthData = useMemo(() => { const getMonthData = useMemo(() => {
const year = currentDate.getFullYear(); const year = currentDate.getFullYear();
const month = currentDate.getMonth(); const month = currentDate.getMonth();
...@@ -198,28 +231,12 @@ const ClassSchedule = ({events = [], onDateSelect, onEventPress}) => { ...@@ -198,28 +231,12 @@ const ClassSchedule = ({events = [], onDateSelect, onEventPress}) => {
}; };
}, [currentDate]); }, [currentDate]);
const getEventsForDate = (date) => { // L2: Hàm kiểm tra ngày tháng
const dateStr = formatDateToString(date); const isCurrentMonth = 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(); return date.getMonth() === currentDate.getMonth();
}; };
const isToday = (date) => { const isToday = date => {
const today = new Date(); const today = new Date();
return ( return (
date.getDate() === today.getDate() && date.getDate() === today.getDate() &&
...@@ -228,20 +245,32 @@ const ClassSchedule = ({events = [], onDateSelect, onEventPress}) => { ...@@ -228,20 +245,32 @@ const ClassSchedule = ({events = [], onDateSelect, onEventPress}) => {
); );
}; };
const navigateMonth = (direction) => { // ==================== HỆ THỐNG ANIMATION ====================
const newDate = new Date(currentDate); // A1: Thiết lập PanResponder
if (direction === 'prev') { const panResponder = useRef(
newDate.setMonth(newDate.getMonth() - 1); PanResponder.create({
} else { onMoveShouldSetPanResponder: (evt, gestureState) => {
newDate.setMonth(newDate.getMonth() + 1); return Math.abs(gestureState.dy) > 10;
} },
setCurrentDate(newDate); onPanResponderMove: (evt, gestureState) => {
setSelectedDate(null); if (gestureState.dy > 0) {
if (showBottomSheet) { bottomSheetTranslateY.setValue(gestureState.dy);
hideBottomSheetModal(); }
} },
}; onPanResponderRelease: (evt, gestureState) => {
if (gestureState.dy > 100) {
hideBottomSheetModal();
} else {
Animated.spring(bottomSheetTranslateY, {
toValue: 0,
useNativeDriver: true,
}).start();
}
},
}),
).current;
// A2: Hàm animation Bottom Sheet
const showBottomSheetModal = () => { const showBottomSheetModal = () => {
setShowBottomSheet(true); setShowBottomSheet(true);
Animated.spring(bottomSheetTranslateY, { Animated.spring(bottomSheetTranslateY, {
...@@ -262,10 +291,33 @@ const ClassSchedule = ({events = [], onDateSelect, onEventPress}) => { ...@@ -262,10 +291,33 @@ const ClassSchedule = ({events = [], onDateSelect, onEventPress}) => {
}); });
}; };
const handleDatePress = (date) => { // ==================== XỬ LÝ SỰ KIỆN ====================
// X1: Xử lý điều hướng
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);
// Phát sự kiện để cập nhật header title
DeviceEventEmitter.emit('onDateChange', newDate.toISOString());
// Cập nhật header drawer với tháng mới
DeviceEventEmitter.emit('updateHeaderMonth', newDate.getMonth());
if (showBottomSheet) {
hideBottomSheetModal();
}
};
// X2: Xử lý chọn ngày
const handleDatePress = date => {
const dateStr = formatDateToString(date); const dateStr = formatDateToString(date);
const dayEvents = getEventsForDate(date); const dayEvents = getEventsForDate(date);
setSelectedDate(dateStr); setSelectedDate(dateStr);
onDateSelect?.(dateStr); onDateSelect?.(dateStr);
...@@ -274,7 +326,8 @@ const ClassSchedule = ({events = [], onDateSelect, onEventPress}) => { ...@@ -274,7 +326,8 @@ const ClassSchedule = ({events = [], onDateSelect, onEventPress}) => {
} }
}; };
const handleEventPress = (event) => { // X3: Xử lý tương tác sự kiện
const handleEventPress = event => {
onEventPress?.(event); onEventPress?.(event);
}; };
...@@ -282,13 +335,6 @@ const ClassSchedule = ({events = [], onDateSelect, onEventPress}) => { ...@@ -282,13 +335,6 @@ const ClassSchedule = ({events = [], onDateSelect, onEventPress}) => {
hideBottomSheetModal(); hideBottomSheetModal();
}; };
const getSelectedEvents = () => {
if (!selectedDate) return [];
return allEvents
.filter(event => event.date === selectedDate)
.sort((a, b) => a.time.localeCompare(b.time));
};
return ( return (
<ClassScheduleView <ClassScheduleView
currentDate={currentDate} currentDate={currentDate}
...@@ -300,6 +346,7 @@ const ClassSchedule = ({events = [], onDateSelect, onEventPress}) => { ...@@ -300,6 +346,7 @@ const ClassSchedule = ({events = [], onDateSelect, onEventPress}) => {
getEventsForDate={getEventsForDate} getEventsForDate={getEventsForDate}
parseLocalDate={parseLocalDate} parseLocalDate={parseLocalDate}
formatDateToDisplay={formatDateToDisplay} formatDateToDisplay={formatDateToDisplay}
formatDateToString={formatDateToString}
isCurrentMonth={isCurrentMonth} isCurrentMonth={isCurrentMonth}
isToday={isToday} isToday={isToday}
navigateMonth={navigateMonth} navigateMonth={navigateMonth}
...@@ -311,4 +358,4 @@ const ClassSchedule = ({events = [], onDateSelect, onEventPress}) => { ...@@ -311,4 +358,4 @@ const ClassSchedule = ({events = [], onDateSelect, onEventPress}) => {
); );
}; };
export default ClassSchedule; export default ClassSchedule;
\ No newline at end of file
...@@ -3,225 +3,263 @@ import R from '../../assets/R'; ...@@ -3,225 +3,263 @@ import R from '../../assets/R';
const {width: screenWidth, height: screenHeight} = Dimensions.get('window'); const {width: screenWidth, height: screenHeight} = Dimensions.get('window');
const CELL_WIDTH = (screenWidth - 30) / 7; const CELL_WIDTH = (screenWidth - 30) / 7;
const CELL_HEIGHT = (screenHeight - 140) / 6; const CELL_HEIGHT = (screenHeight - 160) / 6;
const BOTTOM_SHEET_HEIGHT = screenHeight * 0.6; const BOTTOM_SHEET_HEIGHT = screenHeight * 0.6;
const styles = StyleSheet.create({ const styles = StyleSheet.create({
// Container chính của màn hình
container: { container: {
flex: 1, flex: 1,
backgroundColor: R.colors.white, backgroundColor: R.colors.white,
alignItems: 'center', alignItems: 'center',
}, },
// Header tháng/năm với nút điều hướng
header: { header: {
flexDirection: 'row', flexDirection: 'row',
alignItems: 'center', alignItems: 'center',
justifyContent: 'space-between', justifyContent: 'space-between',
paddingVertical: 15, paddingVertical: 15,
}, },
header_title: { header_title: {
fontSize: R.fontsize.fontsSize16, fontSize: R.fontsize.fontsSize14,
fontFamily: R.fonts.InterMedium, fontFamily: R.fonts.InterMedium,
color: R.colors.black, color: R.colors.black,
fontWeight: '600', fontWeight: '600',
}, },
navButton: { // Nút điều hướng tháng trước/sau
width: 30, navButton: {
height: 30, width: 30,
borderRadius: 20, height: 30,
backgroundColor: R.colors.blue500, borderRadius: 20,
alignItems: 'center', alignItems: 'center',
justifyContent: 'center', justifyContent: 'center',
}, },
navButtonText: { // Tiêu đề các ngày trong tuần
color: R.colors.white, weekDaysContainer: {
fontSize: R.fontsize.fontsSize16, flexDirection: 'row',
fontFamily: R.fonts.InterMedium, paddingBottom: 5,
}, marginBottom: 5,
weekDaysContainer: { },
flexDirection: 'row', weekDayCell: {
paddingBottom: 5, width: CELL_WIDTH,
marginBottom: 5, alignItems: 'center',
}, },
weekDayCell: { weekDayText: {
width: CELL_WIDTH, fontFamily: R.fonts.InterMedium,
alignItems: 'center', fontSize: R.fontsize.fontsSize12,
}, fontWeight: '600',
weekDayText: { color: R.colors.black,
fontFamily: R.fonts.InterRegular, },
fontSize: R.fontsize.fontsSize10,
fontWeight: '400', // Lưới lịch
color: R.colors.black, calendarGrid: {},
}, weekRow: {
calendarGrid: { flexDirection: 'row',
}, },
weekRow: {
flexDirection: 'row', // Ô ngày trong lịch
}, dayCell: {
dayCell: { width: CELL_WIDTH,
width: CELL_WIDTH, minHeight: CELL_HEIGHT,
minHeight: CELL_HEIGHT, borderWidth: 1,
borderWidth: 1, borderColor: R.colors.gray150,
borderColor: R.colors.grey_200, padding: 6,
padding: 4, alignItems: 'center',
alignItems: 'center', },
}, // Ô ngày được chọn
selectedDayCell: { selectedDayCell: {
borderColor: R.colors.blue500, borderColor: R.colors.main,
borderWidth: 1, borderWidth: 1,
}, },
dayText: { // Text số ngày
fontSize: R.fontsize.fontsSize12, dayText: {
fontWeight: '500', fontSize: R.fontsize.fontsSize12,
fontFamily:R.fonts.InterMedium, fontWeight: '400',
color: R.colors.black, fontFamily: R.fonts.InterRegular,
marginBottom: 2, color: R.colors.black,
}, marginBottom: 2,
dayTextInactive: { },
color: R.colors.grey_100, // Text ngày không thuộc tháng hiện tại
opacity: 1, dayTextInactive: {
}, color: R.colors.black,
selectedDayText: { opacity: 1,
color: R.colors.black, },
fontWeight: 'bold', // Text ngày được chọn
fontFamily: R.fonts.InterSemiBold, selectedDayText: {
}, color: R.colors.black,
todayText: { fontWeight: '500',
color: R.colors.white, fontFamily: R.fonts.InterSemiBold,
fontWeight: 'bold', },
fontFamily: R.fonts.InterSemiBold, // Text ngày hôm nay
backgroundColor: R.colors.blue500, todayText: {
borderRadius: 10, color: R.colors.white,
paddingHorizontal: 5, fontWeight: '600',
}, fontFamily: R.fonts.fontSemiBold,
backgroundColor: R.colors.main,
eventsContainer: { borderRadius: 15,
width: '100%', paddingHorizontal: 6,
flex: 1, paddingVertical: 4,
}, },
eventBar: {
paddingVertical: 2, // Sự kiện trong ô ngày
paddingHorizontal: 5, eventsContainer: {
borderRadius: 5, width: '100%',
marginBottom: 2, flex: 1,
}, },
eventBarText: { // Thanh sự kiện nhỏ trong ô ngày
fontSize: R.fontsize.fontsSize10, eventBar: {
color: R.colors.white, paddingVertical: 2,
fontWeight: '500', paddingHorizontal: 5,
fontFamily: R.fonts.InterRegular borderRadius: 10,
}, marginBottom: 2,
moreEventsText: { backgroundColor: R.colors.main,
fontSize: R.fontsize.fontsSize10, },
color: R.colors.grey_100, eventBarText: {
textAlign: 'center', fontSize: R.fontsize.fontsSize12,
}, color: R.colors.white,
fontWeight: '400',
modalBackdrop: { fontFamily: R.fonts.InterRegular,
flex: 1, },
backgroundColor: R.colors.grey_200, // Text hiển thị số sự kiện còn lại
justifyContent: 'flex-end', moreEventsText: {
}, fontSize: R.fontsize.fontsSize12,
bottomSheet: { color: R.colors.gray150,
height: BOTTOM_SHEET_HEIGHT, fontWeight: '400',
backgroundColor: R.colors.white, fontFamily: R.fonts.InterRegular,
borderTopLeftRadius: 20, textAlign: 'center',
borderTopRightRadius: 20, },
}, // Modal bottom sheet
bottomSheetContent: { modalBackdrop: {
paddingHorizontal: 15, flex: 1,
}, backgroundColor: R.colors.black250,
dragHandle: { justifyContent: 'flex-end',
width: 40, },
height: 4, bottomSheet: {
backgroundColor: R.colors.grey_200, height: BOTTOM_SHEET_HEIGHT,
borderRadius: 2, backgroundColor: R.colors.white,
alignSelf: 'center', borderTopLeftRadius: 20,
marginTop: 10, borderTopRightRadius: 20,
marginBottom: 15, },
},
bottomSheetHeader: { // Nội dung bottom sheet
flexDirection: 'row', bottomSheetContent: {
alignItems: 'center', height: BOTTOM_SHEET_HEIGHT,
justifyContent: 'space-between', },
marginBottom: 20, // Thanh kéo
}, dragHandle: {
bottomSheetTitle: { width: 40,
fontSize: R.fontsize.fontSizeHeader1, height: 4,
fontFamily: R.fonts.InterMedium, backgroundColor: R.colors.gray150,
color: R.colors.black, borderRadius: 5,
flex: 1, alignSelf: 'center',
}, marginTop: 10,
closeButton: { marginBottom: 15,
width: 30, },
height: 30, // Header của bottom sheet
borderRadius: 15, bottomSheetHeader: {
backgroundColor: R.colors.grey_200, flexDirection: 'row',
alignItems: 'center', alignItems: 'center',
justifyContent: 'center', justifyContent: 'space-between',
}, paddingHorizontal: 15,
closeButtonText: { },
fontSize: R.fontsize.fontsSize12, bottomSheetTitle: {
color: R.colors.grey_800, fontSize: R.fontsize.fontsSize12,
fontFamily: R.fonts.InterRegular, fontFamily: R.fonts.InterMedium,
}, color: R.colors.black,
fontWeight: '600',
noEventsContainer: { flex: 1,
flex: 1, },
alignItems: 'center', // Nút đóng bottom sheet
justifyContent: 'center', closeButton: {
paddingVertical: 40, width: 30,
}, height: 30,
noEventsText: { borderRadius: 15,
fontSize: R.fontsize.fontsSize12, backgroundColor: R.colors.gray150,
fontFamily: R.fonts.InterRegular, alignItems: 'center',
color: R.colors.grey_800, justifyContent: 'center',
fontWeight: '400' },
}, closeButtonText: {
eventCard: { fontSize: R.fontsize.fontsSize12,
flexDirection: 'row', color: R.colors.black,
backgroundColor: R.colors.white, fontFamily: R.fonts.InterRegular,
borderRadius: 12, fontWeight: '400',
padding: 15, },
marginBottom: 12,
borderLeftWidth: 4, // Danh sách sự kiện trong bottom sheet
borderLeftColor: R.colors.blue500, // ScrollView chứa danh sách sự kiện
shadowColor: R.colors.black, eventsScrollView: {
shadowOffset: { paddingTop: 10,
width: 0, },
height: 2, // Container cho từng sự kiện
}, containerBottomSheet: {
shadowOpacity: 1, flex: 1,
shadowRadius: 1, marginBottom: 10,
elevation: 2, },
}, // Trạng thái không có sự kiện
eventTimeContainer: { noEventsContainer: {
minWidth: 80, flex: 1,
alignItems: 'flex-start', alignItems: 'center',
justifyContent: 'flex-start', justifyContent: 'center',
marginRight: 15, paddingVertical: 40,
}, },
eventTime: { noEventsText: {
fontSize: R.fontsize.fontsSize12, fontSize: R.fontsize.fontsSize12,
fontFamily: R.fonts.InterMedium, fontFamily: R.fonts.InterRegular,
color: R.colors.blue500, color: R.colors.gray150,
fontWeight: '600', fontWeight: '400',
}, },
eventContent: {
flex: 1, // Card sự kiện chi tiết
}, eventCard: {
eventTitle: { flexDirection: 'row',
fontSize: R.fontsize.fontsSize12, backgroundColor: R.colors.white,
fontFamily: R.fonts.InterMedium, borderRadius: 12,
color: R.colors.black, padding: 15,
fontWeight: '600', marginBottom: 10,
marginBottom: 4, marginHorizontal: 15,
}, borderLeftWidth: 4,
eventDescription: { borderLeftColor: R.colors.main,
fontSize: R.fontsize.fontsSize12, shadowColor: R.colors.black,
fontFamily: R.fonts.InterRegular, shadowOffset: {
color: R.colors.grey_800, width: 0,
}, height: 2,
},
shadowOpacity: 1,
shadowRadius: 1,
elevation: 2,
},
// Container thời gian sự kiện
eventTimeContainer: {
minWidth: 80,
alignItems: 'flex-start',
justifyContent: 'flex-start',
marginRight: 15,
},
eventTime: {
fontSize: R.fontsize.fontsSize12,
fontFamily: R.fonts.InterMedium,
color: R.colors.main,
fontWeight: '600',
},
// Container nội dung sự kiện
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.gray200,
fontWeight: '400',
},
}); });
export {styles, CELL_WIDTH, BOTTOM_SHEET_HEIGHT}; export {styles, CELL_WIDTH, BOTTOM_SHEET_HEIGHT};
...@@ -3,20 +3,23 @@ import { ...@@ -3,20 +3,23 @@ import {
Text, Text,
View, View,
TouchableOpacity, TouchableOpacity,
StyleSheet,
ScrollView, ScrollView,
Dimensions,
Modal, Modal,
Animated, Animated,
SafeAreaView, LogBox,
PanResponder,
Image
} from 'react-native'; } from 'react-native';
import R from '../../assets/R'; import R from '../../assets/R';
import { styles, CELL_WIDTH, BOTTOM_SHEET_HEIGHT } from './style'; import {styles} from './style';
import { useNavigation } from '@react-navigation/native'; import {useNavigation} from '@react-navigation/native';
import * as SCREENNAME from '../../routers/ScreenNames'; import * as SCREENNAME from '../../routers/ScreenNames';
import { monthNames } from '../../config/Functions';
LogBox.ignoreLogs([
'[Reanimated] Reduced motion setting is enabled',
'Each child in a list should have a unique "key" prop'
]);
const ClassScheduleView = ({ const ClassScheduleView = ({
currentDate,
selectedDate, selectedDate,
showBottomSheet, showBottomSheet,
bottomSheetTranslateY, bottomSheetTranslateY,
...@@ -25,6 +28,7 @@ const ClassScheduleView = ({ ...@@ -25,6 +28,7 @@ const ClassScheduleView = ({
getEventsForDate, getEventsForDate,
parseLocalDate, parseLocalDate,
formatDateToDisplay, formatDateToDisplay,
formatDateToString,
isCurrentMonth, isCurrentMonth,
isToday, isToday,
navigateMonth, navigateMonth,
...@@ -34,18 +38,38 @@ const ClassScheduleView = ({ ...@@ -34,18 +38,38 @@ const ClassScheduleView = ({
getSelectedEvents, getSelectedEvents,
}) => { }) => {
const navigation = useNavigation(); 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',
];
// Tạo PanResponder cho swipe gesture điều hướng tháng
const swipePanResponder = PanResponder.create({
onMoveShouldSetPanResponder: (evt, gestureState) => {
// Chỉ kích hoạt khi vuốt ngang với khoảng cách đủ lớn
return Math.abs(gestureState.dx) > Math.abs(gestureState.dy) && Math.abs(gestureState.dx) > 20;
},
onPanResponderMove: (evt, gestureState) => {
// Có thể thêm animation preview ở đây nếu cần
},
onPanResponderRelease: (evt, gestureState) => {
const swipeThreshold = 50; // Ngưỡng tối thiểu để kích hoạt swipe
if (gestureState.dx > swipeThreshold) {
// Vuốt phải -> tháng trước
navigateMonth('prev');
} else if (gestureState.dx < -swipeThreshold) {
// Vuốt trái -> tháng sau
navigateMonth('next');
}
},
});
const renderHeader = () => {
return ( return (
<View style={styles.header}> <View style={styles.header}>
<TouchableOpacity <TouchableOpacity
style={styles.navButton} style={styles.navButton}
onPress={() => navigateMonth('prev')}> onPress={() => navigateMonth('prev')}>
<Text style={styles.navButtonText}></Text> <Image
source={R.images.icLeft}
style={styles.icBack}
/>
</TouchableOpacity> </TouchableOpacity>
<Text style={styles.header_title}> <Text style={styles.header_title}>
...@@ -55,7 +79,10 @@ const ClassScheduleView = ({ ...@@ -55,7 +79,10 @@ const ClassScheduleView = ({
<TouchableOpacity <TouchableOpacity
style={styles.navButton} style={styles.navButton}
onPress={() => navigateMonth('next')}> onPress={() => navigateMonth('next')}>
<Text style={styles.navButtonText}></Text> <Image
source={R.images.icRight}
style={styles.icBack}
/>
</TouchableOpacity> </TouchableOpacity>
</View> </View>
); );
...@@ -75,7 +102,7 @@ const ClassScheduleView = ({ ...@@ -75,7 +102,7 @@ const ClassScheduleView = ({
); );
}; };
const renderDayCell = (date, index) => { const renderDayCell = (date, index, formatDateToString) => {
const dayEvents = getEventsForDate(date); const dayEvents = getEventsForDate(date);
const isSelected = selectedDate === formatDateToString(date); const isSelected = selectedDate === formatDateToString(date);
const isTodayDate = isToday(date); const isTodayDate = isToday(date);
...@@ -88,10 +115,11 @@ const ClassScheduleView = ({ ...@@ -88,10 +115,11 @@ const ClassScheduleView = ({
styles.dayCell, styles.dayCell,
isSelected && styles.selectedDayCell, isSelected && styles.selectedDayCell,
isTodayDate && styles.todayCell, isTodayDate && styles.todayCell,
!isInCurrentMonth && {backgroundColor: R.colors.gray220},
]} ]}
onPress={() => handleDatePress(date)} onPress={() => handleDatePress(date)}
activeOpacity={0.7}> activeOpacity={0.7}
>
<Text <Text
style={[ style={[
styles.dayText, styles.dayText,
...@@ -107,10 +135,7 @@ const ClassScheduleView = ({ ...@@ -107,10 +135,7 @@ const ClassScheduleView = ({
{dayEvents.slice(0, 2).map((event, eventIndex) => ( {dayEvents.slice(0, 2).map((event, eventIndex) => (
<TouchableOpacity <TouchableOpacity
key={event.id} key={event.id}
style={[ style={[styles.eventBar]}
styles.eventBar,
{ backgroundColor: R.colors.main },
]}
onPress={() => handleEventPress(event)}> onPress={() => handleEventPress(event)}>
<Text style={styles.eventBarText} numberOfLines={1}> <Text style={styles.eventBarText} numberOfLines={1}>
{event.title} {event.title}
...@@ -126,14 +151,6 @@ const ClassScheduleView = ({ ...@@ -126,14 +151,6 @@ const ClassScheduleView = ({
); );
}; };
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 renderCalendarGrid = () => {
const weeks = []; const weeks = [];
for (let i = 0; i < 6; i++) { for (let i = 0; i < 6; i++) {
...@@ -141,7 +158,7 @@ const ClassScheduleView = ({ ...@@ -141,7 +158,7 @@ const ClassScheduleView = ({
weeks.push( weeks.push(
<View key={i} style={styles.weekRow}> <View key={i} style={styles.weekRow}>
{week.map((date, dayIndex) => {week.map((date, dayIndex) =>
renderDayCell(date, i * 7 + dayIndex), renderDayCell(date, i * 7 + dayIndex, formatDateToString),
)} )}
</View>, </View>,
); );
...@@ -157,7 +174,7 @@ const ClassScheduleView = ({ ...@@ -157,7 +174,7 @@ const ClassScheduleView = ({
return ( return (
<View style={styles.bottomSheetContent}> <View style={styles.bottomSheetContent}>
<View style={styles.dragHandle} /> <View style={styles.dragHandle}></View>
<View style={styles.bottomSheetHeader}> <View style={styles.bottomSheetHeader}>
<Text style={styles.bottomSheetTitle}> <Text style={styles.bottomSheetTitle}>
...@@ -169,7 +186,6 @@ const ClassScheduleView = ({ ...@@ -169,7 +186,6 @@ const ClassScheduleView = ({
<Text style={styles.closeButtonText}></Text> <Text style={styles.closeButtonText}></Text>
</TouchableOpacity> </TouchableOpacity>
</View> </View>
<ScrollView <ScrollView
style={styles.eventsScrollView} style={styles.eventsScrollView}
showsVerticalScrollIndicator={false}> showsVerticalScrollIndicator={false}>
...@@ -179,30 +195,33 @@ const ClassScheduleView = ({ ...@@ -179,30 +195,33 @@ const ClassScheduleView = ({
</View> </View>
) : ( ) : (
selectedEvents.map((event, index) => ( selectedEvents.map((event, index) => (
<TouchableOpacity <View style={styles.containerBottomSheet}>
key={event.id} <TouchableOpacity
style={styles.eventCard} key={event.id}
onPress={() => navigation.navigate(SCREENNAME.DETAILCLASSSCHEDULE, { event })} style={styles.eventCard}
activeOpacity={0.7}> onPress={() =>
navigation.navigate(SCREENNAME.DETAILSCHEDULE, {event})
<View style={styles.eventTimeContainer}> }
<Text style={styles.eventTime}> activeOpacity={0.7}>
{event.time} <View style={styles.eventTimeContainer}>
{event.endTime && ` - ${event.endTime}`} <Text style={styles.eventTime}>
</Text> {event.time}
</View> {event.endTime && ` - ${event.endTime}`}
</Text>
</View>
<View style={styles.eventContent}> <View style={styles.eventContent}>
<Text style={styles.eventTitle} numberOfLines={2}> <Text style={styles.eventTitle} numberOfLines={2}>
{event.title} {event.title}
</Text>
{event.description && (
<Text style={styles.eventDescription} numberOfLines={3}>
{event.description}
</Text> </Text>
)} {event.description && (
</View> <Text style={styles.eventDescription} numberOfLines={3}>
</TouchableOpacity> {event.description}
</Text>
)}
</View>
</TouchableOpacity>
</View>
)) ))
)} )}
</ScrollView> </ScrollView>
...@@ -210,6 +229,8 @@ const ClassScheduleView = ({ ...@@ -210,6 +229,8 @@ const ClassScheduleView = ({
); );
}; };
// ===== RENDER BOTTOM SHEET - Modal bottom sheet =====
// Sử dụng styles: modalBackdrop, bottomSheet
const renderBottomSheet = () => { const renderBottomSheet = () => {
return ( return (
<Modal <Modal
...@@ -217,17 +238,15 @@ const ClassScheduleView = ({ ...@@ -217,17 +238,15 @@ const ClassScheduleView = ({
transparent={true} transparent={true}
animationType="none" animationType="none"
onRequestClose={handleCloseBottomSheet}> onRequestClose={handleCloseBottomSheet}>
<TouchableOpacity <TouchableOpacity
style={styles.modalBackdrop} style={styles.modalBackdrop}
activeOpacity={1} activeOpacity={1}
onPress={handleCloseBottomSheet}> onPress={handleCloseBottomSheet}>
<Animated.View <Animated.View
style={[ style={[
styles.bottomSheet, styles.bottomSheet,
{ {
transform: [{ translateY: bottomSheetTranslateY }], transform: [{translateY: bottomSheetTranslateY}],
}, },
]} ]}
{...panResponder.panHandlers}> {...panResponder.panHandlers}>
...@@ -240,19 +259,20 @@ const ClassScheduleView = ({ ...@@ -240,19 +259,20 @@ const ClassScheduleView = ({
); );
}; };
// ===== MAIN RENDER - Render chính của component =====
// Sử dụng styles: container
return ( return (
<View style={styles.container}>
<SafeAreaView style={styles.container}>
<ScrollView showsVerticalScrollIndicator={false}> <ScrollView showsVerticalScrollIndicator={false}>
{renderHeader()} <View style={styles.body} {...swipePanResponder.panHandlers}>
{renderWeekDays()} {renderHeader()}
{renderCalendarGrid()} {renderWeekDays()}
{renderCalendarGrid()}
</View>
</ScrollView> </ScrollView>
{renderBottomSheet()} {renderBottomSheet()}
</SafeAreaView> </View>
); );
}; };
export default ClassScheduleView;
export default ClassScheduleView;
\ No newline at end of file
...@@ -89,7 +89,7 @@ const styles = StyleSheet.create({ ...@@ -89,7 +89,7 @@ const styles = StyleSheet.create({
backgroundColor: R.colors.blue500, backgroundColor: R.colors.blue500,
}, },
avatar_text: { avatar_text: {
fontSize: R.fontsize.fontsSize16, fontSize: R.fontsize.fontsSize14,
fontWeight: '600', fontWeight: '600',
color: R.colors.white, color: R.colors.white,
fontFamily: R.fonts.InterSemiBold, fontFamily: R.fonts.InterSemiBold,
...@@ -100,9 +100,9 @@ const styles = StyleSheet.create({ ...@@ -100,9 +100,9 @@ const styles = StyleSheet.create({
}, },
text_card_info: { text_card_info: {
fontSize: R.fontsize.fontsSize12, fontSize: R.fontsize.fontsSize12,
fontWeight: '600', fontWeight: '400',
color: R.colors.black, color: R.colors.black,
fontFamily: R.fonts.InterMedium, fontFamily: R.fonts.InterRegular,
numberOfLines: 1, numberOfLines: 1,
ellipsizeMode: 'tail', ellipsizeMode: 'tail',
}, },
...@@ -110,15 +110,14 @@ const styles = StyleSheet.create({ ...@@ -110,15 +110,14 @@ const styles = StyleSheet.create({
paddingVertical: 5, paddingVertical: 5,
paddingHorizontal: 10, paddingHorizontal: 10,
backgroundColor: R.colors.gray220, backgroundColor: R.colors.gray220,
minHeight: 21, height:30,
maxWidth: 108,
borderRadius: 15, borderRadius: 15,
flexDirection: 'row', flexDirection: 'row',
justifyContent: 'center', justifyContent: 'center',
alignItems: 'center', alignItems: 'center',
}, },
btn_text: { btn_text: {
fontSize: R.fontsize.fontsSize10, fontSize: R.fontsize.fontsSize12,
fontWeight: '400', fontWeight: '400',
color: R.colors.black, color: R.colors.black,
fontFamily: R.fonts.InterRegular, fontFamily: R.fonts.InterRegular,
......
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