Skip to content

Commit

Permalink
Markoh/tppdev 56 prediction and notification ovulation prediction (#188)
Browse files Browse the repository at this point in the history
* Added Marked dates

* checkpoint

* fixed up notification toggle

* Updates for notification toggle

* Identify next ovulation dates

* colored the ovulation dates

* Added oviulation prediction shading

* So far

* moved code from log multiple days to calendar screen

* moved code from logmultipledays to calendar screen

* removed redundancy

* removed redundant code

* Revert changes to package-lock.json

* removed tests

* Revert changes to package-lock.json

* removed more test files

* readded Calculation Test

* removed CycleServiceTest

* removed redundancy

* Renew pr

* Readded some useEffect call to settings.js

* pred ovulation color

---------

Co-authored-by: Merrick Liu <[email protected]>
  • Loading branch information
marko0124 and merrickliu888 authored Feb 16, 2025
1 parent 92372a7 commit e01d189
Show file tree
Hide file tree
Showing 14 changed files with 689 additions and 95 deletions.
15 changes: 15 additions & 0 deletions tpp-app/App.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,12 @@ import {
GETRemindLogSymptomsFreq,
GETRemindLogSymptomsTime,
GETRemindLogSymptoms,
POSTRemindOvulation,
POSTRemindOvulationFreq,
POSTRemindOvulationTime,
GETRemindOvulationFreq,
GETRemindOvulationTime,
GETRemindOvulation,
} from "./src/services/SettingsService";
import SettingsIcon from "./assets/icons/settings_icon.svg";
import InfoIcon from "./assets/icons/info_icon.svg";
Expand Down Expand Up @@ -181,6 +187,15 @@ function App() {
if ((await GETRemindLogSymptoms()) === null) {
await POSTRemindLogSymptoms(true);
}
if ((await GETRemindOvulationFreq()) === null) {
await POSTRemindLogSymptomsFreq("Every day");
}
if ((await GETRemindOvulationTime()) == null) {
await POSTRemindOvulationTime("10:00 AM");
}
if ((await GETRemindOvulation()) === null) {
await POSTRemindOvulation(true);
}
},
(data) => {
console.log("PushNotificationsIOS.requestPermissions failed", data);
Expand Down
22 changes: 11 additions & 11 deletions tpp-app/jest.config.js
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
module.exports = {
preset: "react-native",
moduleFileExtensions: ["ts", "tsx", "js", "jsx", "json", "node"],
transformIgnorePatterns: [
"node_modules/(?!(@react-native|react-native" +
"|react-navigation-tabs" +
"|react-native-splash-screen" +
"|react-native-screens" +
"|react-native-reanimated" +
")/)",
],
};
preset: "react-native",
moduleFileExtensions: ["ts", "tsx", "js", "jsx", "json", "node"],
transformIgnorePatterns: [
"node_modules/(?!(@react-native|react-native" +
"|react-navigation-tabs" +
"|react-native-splash-screen" +
"|react-native-screens" +
"|react-native-reanimated" +
")/)",
],
};
2 changes: 1 addition & 1 deletion tpp-app/src/home/CalendarNavigator.js
Original file line number Diff line number Diff line change
Expand Up @@ -59,4 +59,4 @@ export default function CalendarNavigator() {
<Stack.Screen name={CALENDAR_STACK_SCREENS.LEGEND_PAGE} component={LegendScreen} />
</Stack.Navigator>
);
}
}
146 changes: 92 additions & 54 deletions tpp-app/src/home/components/DayComponent.js
Original file line number Diff line number Diff line change
Expand Up @@ -40,81 +40,115 @@ import { OvulatingIcon } from "../../services/utils/calendaricons";
import { VIEWS } from "../../services/utils/constants";
import { FILTER_COLOURS, FILTER_TEXT_COLOURS } from "../../services/utils/constants";
import { CALENDAR_STACK_SCREENS } from "../CalendarNavigator";
import { Filter } from "react-native-svg";

// The component that is used by each day in the calendar
export const DayComponent = ({ date, state, marking, selectedView, navigation }) => {
let bgColor;
let textColor;
let iconName = "view";
let renderedIcon;
let isDisabled = false;

// If this specific date has been marked
if (marking) {
// View key just tells us what the view is set to
let viewKey = getKeyByValue(VIEWS, selectedView).toLowerCase();
// Basically whatever special value that is attached to the specific key in the Symptoms object
// i.e. for flow it would be HEAVY/MEDIUM/LIGHT
// for sleep it will be a number etc.
let symptomAttribute = marking.symptoms ? marking.symptoms[viewKey] : null;

// If disabled
if (marking.disable) {
bgColor = FILTER_COLOURS.DISABLED;
textColor = FILTER_TEXT_COLOURS.DISABLED;
// If it contains a working attribute
} else if (symptomAttribute) {
let attribute = symptomAttribute;

// Sleep and exercise have special cases to be calculated through
switch (viewKey) {
case "sleep":
attribute = filterSleep(attribute);
break;
case "exercise":
attribute = filterExercise(symptomAttribute.exercise_minutes);
if (symptomAttribute.exercise) {
iconName = viewKey + symptomAttribute.exercise.toLowerCase();
iconName = iconName.replace(/\s+/g, "");
}

break;
}
// Check if this is an ovulation date
const isOvulationDate = marking && marking.ovulation;

if (isOvulationDate) {
// Set ovulation date styling
bgColor = FILTER_COLOURS.OVULATION.OVULATING; // Teal color
textColor = 'white';
isDisabled = true; // Make it non-interactive
} else if (marking) {
// Handle predicted ovulation dates
if (marking.period) {
bgColor = FILTER_COLOURS.OVULATION.PREDICTED_OVULATION;
textColor = FILTER_TEXT_COLOURS.OVULATION.OVULATING;
} else {
// View key just tells us what the view is set to
let viewKey = getKeyByValue(VIEWS, selectedView).toLowerCase();
// Basically whatever special value that is attached to the specific key in the Symptoms object
// i.e. for flow it would be HEAVY/MEDIUM/LIGHT
// for sleep it will be a number etc.
let symptomAttribute = marking.symptoms ? marking.symptoms[viewKey] : null;

// If disabled
if (marking.disable) {
bgColor = FILTER_COLOURS.DISABLED;
textColor = FILTER_TEXT_COLOURS.DISABLED;
// If it contains a working attribute
} else if (symptomAttribute) {
let attribute = symptomAttribute;

// Sleep and exercise have special cases to be calculated through
switch (viewKey) {
case "sleep":
attribute = filterSleep(attribute);
break;
case "exercise":
attribute = filterExercise(symptomAttribute.exercise_minutes);
if (symptomAttribute.exercise) {
iconName = viewKey + symptomAttribute.exercise.toLowerCase();
iconName = iconName.replace(/\s+/g, "");
}
break;
}

// Mood is the only one that does not modify the background colour
if (viewKey !== "mood") {
bgColor = FILTER_COLOURS[viewKey.toUpperCase()][attribute.toUpperCase()];
textColor = FILTER_TEXT_COLOURS[viewKey.toUpperCase()][attribute.toUpperCase()];
} else {
bgColor = FILTER_COLOURS.NOFILTER;
textColor = FILTER_TEXT_COLOURS.NOFILTER;
}

// Get Icon
if (viewKey !== "sleep" && viewKey !== "exercise") {
iconName = viewKey + symptomAttribute.toLowerCase();
}

// Mood is the only one that does not modify the background colour
if (viewKey !== "mood") {
bgColor = FILTER_COLOURS[viewKey.toUpperCase()][attribute.toUpperCase()];
textColor = FILTER_TEXT_COLOURS[viewKey.toUpperCase()][attribute.toUpperCase()];
renderedIcon = createElement(ICON_TYPES[iconName], {
style: styles.dayIcon,
width: ICON_SIZE.width,
height: ICON_SIZE.height,
fill: textColor,
});
} else {
bgColor = FILTER_COLOURS.NOFILTER;
textColor = FILTER_TEXT_COLOURS.NOFILTER;
}

// Get Icon
if (viewKey !== "sleep" && viewKey !== "exercise") {
iconName = viewKey + symptomAttribute.toLowerCase();
}

renderedIcon = createElement(ICON_TYPES[iconName], {
style: styles.dayIcon,
width: ICON_SIZE.width,
height: ICON_SIZE.height,
fill: textColor,
});
} else {
bgColor = FILTER_COLOURS.NOFILTER;
textColor = FILTER_TEXT_COLOURS.NOFILTER;
}
}

return (
<TouchableOpacity
disabled={bgColor === FILTER_COLOURS.DISABLED && textColor === FILTER_TEXT_COLOURS.DISABLED}
disabled={isDisabled || marking?.disable}
onPress={() => {
navigation.navigate(CALENDAR_STACK_SCREENS.LOG_SYMPTOMS, { date: date });
if (!isDisabled && !marking?.disable) {
navigation.navigate(CALENDAR_STACK_SCREENS.LOG_SYMPTOMS, {
date: date,
});
}
}}
>
<View style={styles.dayContainer} backgroundColor={bgColor}>
<Text style={{ color: textColor }}>{date.day}</Text>
<View
style={[
styles.dayContainer,
{
backgroundColor: bgColor || FILTER_COLOURS.NOFILTER,
},
]}
>
<Text
style={[
styles.dayText,
{
color: textColor || FILTER_TEXT_COLOURS.NOFILTER,
},
]}
>
{date.day}
</Text>
{renderedIcon}
</View>
</TouchableOpacity>
Expand Down Expand Up @@ -194,6 +228,10 @@ const styles = StyleSheet.create({
marginLeft: "auto",
marginRight: "auto",
},
dayText: {
fontSize: 16,
fontWeight: '400',
},
});

const ICON_SIZE = {
Expand Down
Empty file.
104 changes: 101 additions & 3 deletions tpp-app/src/home/pages/CalendarScreen.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,13 @@ import OnboardingBackground from "../../../assets/SplashScreenBackground/colourw
import LoadingVisual from "../components/LoadingVisual";
import { GETTutorial } from "../../services/TutorialService";
import LegendButton from "../../../assets/icons/legend_icon.svg";
import { addDays } from 'date-fns';
import CycleService from '../../services/cycle/CycleService';
import { calculateAverageOvulationLength } from '../../services/CalculationService';

export let scrollDate = getISODate(new Date());

export const Calendar = ({ navigation, marked, setYearInView, selectedView, route }) => {
export const Calendar = ({ navigation, marked, setYearInView, selectedView, route, ovulationDates }) => {
const jumpDate = route.params?.newDate ? route.params.newDate : getISODate(new Date());
let joinedDate = "";
GETJoinedDate().then((res) => {
Expand All @@ -32,7 +35,7 @@ export const Calendar = ({ navigation, marked, setYearInView, selectedView, rout
// Max amount of months allowed to scroll to the past. Default = 50
pastScrollRange={pastScroll}
// Max amount of months allowed to scroll to the future. Default = 50
futureScrollRange={jumpDate.slice(8, 10) === "01" ? 1 : 0}
futureScrollRange={2}
// Enable or disable scrolling of calendar list
scrollEnabled={true}
// Check which months are currently in view
Expand Down Expand Up @@ -85,7 +88,11 @@ export const Calendar = ({ navigation, marked, setYearInView, selectedView, rout
},
},
}}
markedDates={marked}
markingType={'period'}
markedDates={{
...marked,
...ovulationDates
}}
/>
);
};
Expand All @@ -99,6 +106,7 @@ export default function CalendarScreen({ route, navigation }) {
const [cachedYears, setCachedYears] = useState({});
const [marked, setMarked] = useState({});
const [loaded, setLoaded] = useState(false);
const [ovulationDates, setOvulationDates] = useState({});

useEffect(() => {
async function fetchYearData() {
Expand Down Expand Up @@ -151,6 +159,26 @@ export default function CalendarScreen({ route, navigation }) {
fetchYearData();
}, [yearInView]);

useEffect(() => {
async function markOvulation() {
// 1. get days until ovulation
const daysUntilOvulation = await CycleService.GETPredictedDaysTillOvulation();
if (daysUntilOvulation <= 0) return;
// 2. build your 5-day ovulation window
let ovulationDates = [];
for (let i = 0; i < 5; i++) {
let date = new Date();
date.setDate(date.getDate() + (daysUntilOvulation + i));
ovulationDates.push({
year: date.getFullYear(),
month: date.getMonth() + 1,
day: date.getDate(),
});
}
}
markOvulation();
}, []);

useFocusEffect(
useCallback(() => {
// set newly marked calendar dates with changed symptoms
Expand Down Expand Up @@ -188,6 +216,75 @@ export default function CalendarScreen({ route, navigation }) {
) : (
<Icon name="keyboard-arrow-down" size={24} />
);

const getOvulationDates = async () => {
try {
// Only get ovulation dates if ovulation view is selected
if (selectedView !== VIEWS.Ovulation) {
setOvulationDates({});
return;
}

const daysTillOvulation = await CycleService.GETPredictedDaysTillOvulation();
const today = new Date();
const markedDates = {};
const ovulationLength = calculateAverageOvulationLength() || 5;

// Mark current ovulation if we're in it
if (daysTillOvulation <= 0 && daysTillOvulation >= -ovulationLength) {
const currentOvulationStart = addDays(today, daysTillOvulation);
for (let i = 0; i < ovulationLength + daysTillOvulation; i++) {
const dateToMark = addDays(currentOvulationStart, i);
markedDates[dateToMark.toISOString().split('T')[0]] = {
ovulation: true,
disabled: true,
customStyles: {
container: {
borderRadius: 0,
backgroundColor: '#55ad9e', // ARGB: #0xFF55AD9E
},
text: {
color: 'white',
fontWeight: '400'
}
}
};
}
}

// Mark next ovulation window if predicted
if (daysTillOvulation > 0) {
const nextOvulationDate = addDays(today, daysTillOvulation);

for (let i = 0; i < ovulationLength; i++) {
const dateToMark = addDays(nextOvulationDate, i);
markedDates[dateToMark.toISOString().split('T')[0]] = {
ovulation: true,
disabled: true,
customStyles: {
container: {
borderRadius: 0,
backgroundColor: '#55ad9e',
},
text: {
color: 'white',
fontWeight: '400'
}
}
};
}
}

setOvulationDates(markedDates);
} catch (error) {
console.error('Error getting ovulation dates:', error);
}
};

useEffect(() => {
getOvulationDates();
}, [selectedView]);

if (loaded) {
return (
<ErrorFallback>
Expand Down Expand Up @@ -223,6 +320,7 @@ export default function CalendarScreen({ route, navigation }) {
setYearInView={setYearInView}
selectedView={selectedView}
route={route}
ovulationDates={ovulationDates}
/>
</View>
</SafeAreaView>
Expand Down
Loading

0 comments on commit e01d189

Please sign in to comment.