From b6395b71b99b9797a86396102b12f4f419643a69 Mon Sep 17 00:00:00 2001 From: Maxime RICHARD Date: Fri, 12 Apr 2024 13:43:49 +0200 Subject: [PATCH 1/6] feat: debut page stats --- app/application/_layout.tsx | 6 +-- app/application/habits/history.tsx | 50 ------------------- app/application/habits/stats.tsx | 14 ++++++ presentation/react/components/Stats/Stats.tsx | 35 +++++++++++++ 4 files changed, 52 insertions(+), 53 deletions(-) delete mode 100644 app/application/habits/history.tsx create mode 100644 app/application/habits/stats.tsx create mode 100644 presentation/react/components/Stats/Stats.tsx diff --git a/app/application/_layout.tsx b/app/application/_layout.tsx index 1bf47a0..0bc2965 100644 --- a/app/application/_layout.tsx +++ b/app/application/_layout.tsx @@ -43,11 +43,11 @@ const TabLayout: React.FC = () => { }} /> { - return + return }, }} /> diff --git a/app/application/habits/history.tsx b/app/application/habits/history.tsx deleted file mode 100644 index b4497fa..0000000 --- a/app/application/habits/history.tsx +++ /dev/null @@ -1,50 +0,0 @@ -import { useMemo, useState } from "react" -import { View } from "react-native" -import { Agenda } from "react-native-calendars" -import { Text } from "react-native-paper" -import { SafeAreaView } from "react-native-safe-area-context" - -import { getISODate, getNowDate } from "@/utils/dates" - -const HistoryPage: React.FC = () => { - const today = useMemo(() => { - return getNowDate() - }, []) - const todayISO = getISODate(today) - - const [selectedDate, setSelectedDate] = useState(today) - const selectedISODate = getISODate(selectedDate) - - return ( - - { - setSelectedDate(new Date(date.dateString)) - }} - markedDates={{ - [todayISO]: { marked: true }, - }} - selected={selectedISODate} - renderList={() => { - return ( - - {selectedDate.toISOString()} - - ) - }} - /> - - ) -} - -export default HistoryPage diff --git a/app/application/habits/stats.tsx b/app/application/habits/stats.tsx new file mode 100644 index 0000000..ddb24e4 --- /dev/null +++ b/app/application/habits/stats.tsx @@ -0,0 +1,14 @@ +import { Stats } from "@/presentation/react/components/Stats/Stats" +import { useAuthentication } from "@/presentation/react/contexts/Authentication" + +const StatsPage: React.FC = () => { + const { user } = useAuthentication() + + if (user == null) { + return null + } + + return +} + +export default StatsPage diff --git a/presentation/react/components/Stats/Stats.tsx b/presentation/react/components/Stats/Stats.tsx new file mode 100644 index 0000000..00b00e1 --- /dev/null +++ b/presentation/react/components/Stats/Stats.tsx @@ -0,0 +1,35 @@ +import { SafeAreaView } from "react-native-safe-area-context" +import { Card, Text } from "react-native-paper" + +export const Stats: React.FC = () => { + return ( + + {"Statistique"} + + + + + nbDays Sucess that follow + + + + + + nbDays Sucess + + + + + + nbDays Fail + + + + + + CardContent + + + + ) +} From c03cd2b96db72861984b364843272d9a28e56015 Mon Sep 17 00:00:00 2001 From: Maxime RICHARD Date: Fri, 12 Apr 2024 15:27:11 +0200 Subject: [PATCH 2/6] feat: stats continue --- app/application/habits/stats.tsx | 10 +- package-lock.json | 198 +++++++++++++++++- package.json | 1 + presentation/react/components/Stats/Stats.tsx | 85 +++++--- 4 files changed, 260 insertions(+), 34 deletions(-) diff --git a/app/application/habits/stats.tsx b/app/application/habits/stats.tsx index ddb24e4..73a642a 100644 --- a/app/application/habits/stats.tsx +++ b/app/application/habits/stats.tsx @@ -1,14 +1,10 @@ import { Stats } from "@/presentation/react/components/Stats/Stats" -import { useAuthentication } from "@/presentation/react/contexts/Authentication" +import { useHabitsTracker } from "@/presentation/react/contexts/HabitsTracker" const StatsPage: React.FC = () => { - const { user } = useAuthentication() + const { habitsTracker } = useHabitsTracker() - if (user == null) { - return null - } - - return + return } export default StatsPage diff --git a/package-lock.json b/package-lock.json index 1cdfda5..5c49466 100644 --- a/package-lock.json +++ b/package-lock.json @@ -29,6 +29,7 @@ "react-hook-form": "7.51.2", "react-native": "0.73.6", "react-native-calendars": "1.1304.1", + "react-native-circular-progress-indicator": "4.4.2", "react-native-elements": "3.4.3", "react-native-gesture-handler": "2.14.1", "react-native-paper": "5.12.3", @@ -8627,6 +8628,11 @@ "node": ">=6.5" } }, + "node_modules/abs-svg-path": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/abs-svg-path/-/abs-svg-path-0.1.1.tgz", + "integrity": "sha512-d8XPSGjfyzlXC3Xx891DJRyZfqk5JU0BJrDQcsWomFIV1/BIzPW5HDH5iDdWpqWaav0YVIEzT1RHTwWr0FFshA==" + }, "node_modules/accepts": { "version": "1.3.8", "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", @@ -9596,6 +9602,12 @@ "resolved": "https://registry.npmjs.org/blueimp-md5/-/blueimp-md5-2.19.0.tgz", "integrity": "sha512-DRQrD6gJyy8FbiE4s+bDoXS9hiW3Vbx5uCdwvcCf3zLHL+Iv7LtGHLpr+GZV8rHG8tK766FGYBwRbu8pELTt+w==" }, + "node_modules/boolbase": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", + "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==", + "peer": true + }, "node_modules/bplist-creator": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/bplist-creator/-/bplist-creator-0.1.0.tgz", @@ -10623,6 +10635,56 @@ "hyphenate-style-name": "^1.0.3" } }, + "node_modules/css-select": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/css-select/-/css-select-5.1.0.tgz", + "integrity": "sha512-nwoRF1rvRRnnCqqY7updORDsuqKzqYJ28+oSMaJMMgOauh3fvwHqMS7EZpIPqK8GL+g9mKxF1vP/ZjSeNjEVHg==", + "peer": true, + "dependencies": { + "boolbase": "^1.0.0", + "css-what": "^6.1.0", + "domhandler": "^5.0.2", + "domutils": "^3.0.1", + "nth-check": "^2.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/css-tree": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-1.1.3.tgz", + "integrity": "sha512-tRpdppF7TRazZrjJ6v3stzv93qxRcSsFmW6cX0Zm2NVKpxE1WV1HblnghVv9TreireHkqI/VDEsfolRF1p6y7Q==", + "peer": true, + "dependencies": { + "mdn-data": "2.0.14", + "source-map": "^0.6.1" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/css-tree/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "peer": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/css-what": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/css-what/-/css-what-6.1.0.tgz", + "integrity": "sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw==", + "peer": true, + "engines": { + "node": ">= 6" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, "node_modules/cssom": { "version": "0.5.0", "resolved": "https://registry.npmjs.org/cssom/-/cssom-0.5.0.tgz", @@ -11157,6 +11219,32 @@ "node": ">=6.0.0" } }, + "node_modules/dom-serializer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", + "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==", + "peer": true, + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.2", + "entities": "^4.2.0" + }, + "funding": { + "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" + } + }, + "node_modules/domelementtype": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", + "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "peer": true + }, "node_modules/domexception": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/domexception/-/domexception-4.0.0.tgz", @@ -11170,6 +11258,35 @@ "node": ">=12" } }, + "node_modules/domhandler": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz", + "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==", + "peer": true, + "dependencies": { + "domelementtype": "^2.3.0" + }, + "engines": { + "node": ">= 4" + }, + "funding": { + "url": "https://github.com/fb55/domhandler?sponsor=1" + } + }, + "node_modules/domutils": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.1.0.tgz", + "integrity": "sha512-H78uMmQtI2AhgDJjWeQmHwJJ2bLPD3GMmO7Zja/ZZh84wkm+4ut+IUnUdRa8uCGX88DiVx1j6FRe1XfxEgjEZA==", + "peer": true, + "dependencies": { + "dom-serializer": "^2.0.0", + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3" + }, + "funding": { + "url": "https://github.com/fb55/domutils?sponsor=1" + } + }, "node_modules/dot-prop": { "version": "5.3.0", "resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-5.3.0.tgz", @@ -11255,7 +11372,6 @@ "version": "4.5.0", "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", - "dev": true, "engines": { "node": ">=0.12" }, @@ -17970,6 +18086,12 @@ "resolved": "https://registry.npmjs.org/md5hex/-/md5hex-1.0.0.tgz", "integrity": "sha512-c2YOUbp33+6thdCUi34xIyOU/a7bvGKj/3DB1iaPMTuPHf/Q2d5s4sn1FaCOO43XkXggnb08y5W2PU8UNYNLKQ==" }, + "node_modules/mdn-data": { + "version": "2.0.14", + "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.14.tgz", + "integrity": "sha512-dn6wd0uw5GsdswPFfsgMp5NSB0/aDe6fK94YJV/AJDYXL6HVLWBsxeq7js7Ad+mU2K9LAlwpk6kN2D5mwCPVow==", + "peer": true + }, "node_modules/memoize-one": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-5.2.1.tgz", @@ -18966,6 +19088,14 @@ "node": ">=0.10.0" } }, + "node_modules/normalize-svg-path": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/normalize-svg-path/-/normalize-svg-path-1.1.0.tgz", + "integrity": "sha512-r9KHKG2UUeB5LoTouwDzBy2VxXlHsiM6fyLQvnJa0S5hrhzqElH/CH7TUGhT1fVvIYBIKf3OpY4YJ4CK+iaqHg==", + "dependencies": { + "svg-arc-to-cubic-bezier": "^3.0.0" + } + }, "node_modules/normalize-url": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-2.0.1.tgz", @@ -19055,6 +19185,18 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/nth-check": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", + "integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==", + "peer": true, + "dependencies": { + "boolbase": "^1.0.0" + }, + "funding": { + "url": "https://github.com/fb55/nth-check?sponsor=1" + } + }, "node_modules/nullthrows": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/nullthrows/-/nullthrows-1.1.1.tgz", @@ -19512,6 +19654,11 @@ "node": ">=10" } }, + "node_modules/parse-svg-path": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/parse-svg-path/-/parse-svg-path-0.1.2.tgz", + "integrity": "sha512-JyPSBnkTJ0AI8GGJLfMXvKq42cj5c006fnLz6fXy6zfoVjJizi8BNTpu8on8ziI1cKy9d9DGNuY17Ce7wuejpQ==" + }, "node_modules/parse5": { "version": "7.1.2", "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.1.2.tgz", @@ -20236,6 +20383,20 @@ "moment": "^2.29.4" } }, + "node_modules/react-native-circular-progress-indicator": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/react-native-circular-progress-indicator/-/react-native-circular-progress-indicator-4.4.2.tgz", + "integrity": "sha512-BlgshzIIIk0TP/CZY+5oyRsHl2Sb2l6cK5tzfmrVETfn6pN8dhOKrV0i3Z6i0SM8wRgncdUUSRBpCPAexh7JoQ==", + "dependencies": { + "react-native-redash": "*" + }, + "peerDependencies": { + "react": ">=16.8.0", + "react-native": ">=0.59.0", + "react-native-reanimated": ">=2.2.0", + "react-native-svg": ">=12.1.1" + } + }, "node_modules/react-native-elements": { "version": "3.4.3", "resolved": "https://registry.npmjs.org/react-native-elements/-/react-native-elements-3.4.3.tgz", @@ -20339,6 +20500,22 @@ "react-native": "*" } }, + "node_modules/react-native-redash": { + "version": "18.1.3", + "resolved": "https://registry.npmjs.org/react-native-redash/-/react-native-redash-18.1.3.tgz", + "integrity": "sha512-RKdmAjs87iwA8iD7UOQ11rlQL+tZh56O5+5cV1M37Jk+4uH1Fvs22G3Q5v/wGx+0ncwz3wMolLLCezONOodTlA==", + "dependencies": { + "abs-svg-path": "^0.1.1", + "normalize-svg-path": "^1.0.1", + "parse-svg-path": "^0.1.2" + }, + "peerDependencies": { + "react": "*", + "react-native": "*", + "react-native-gesture-handler": "*", + "react-native-reanimated": ">=2.0.0" + } + }, "node_modules/react-native-safe-area-context": { "version": "4.8.2", "resolved": "https://registry.npmjs.org/react-native-safe-area-context/-/react-native-safe-area-context-4.8.2.tgz", @@ -20369,6 +20546,20 @@ "react-native": "*" } }, + "node_modules/react-native-svg": { + "version": "15.1.0", + "resolved": "https://registry.npmjs.org/react-native-svg/-/react-native-svg-15.1.0.tgz", + "integrity": "sha512-p0Sx0EpQNk1nu6UcMEiB8K9P04n3J7s+pNYUwf1d/Yz+v4hk961VjuVqjyndgiEbHZyWiKWLZRVNuvLpwjPY2A==", + "peer": true, + "dependencies": { + "css-select": "^5.1.0", + "css-tree": "^1.1.3" + }, + "peerDependencies": { + "react": "*", + "react-native": "*" + } + }, "node_modules/react-native-swipe-gestures": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/react-native-swipe-gestures/-/react-native-swipe-gestures-1.0.5.tgz", @@ -22281,6 +22472,11 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/svg-arc-to-cubic-bezier": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/svg-arc-to-cubic-bezier/-/svg-arc-to-cubic-bezier-3.2.0.tgz", + "integrity": "sha512-djbJ/vZKZO+gPoSDThGNpKDO+o+bAeA4XQKovvkNCqnIS2t+S4qnLAGQhyyrulhCFRl1WWzAp0wUDV8PpTVU3g==" + }, "node_modules/symbol-tree": { "version": "3.2.4", "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", diff --git a/package.json b/package.json index 81190b9..db9719f 100644 --- a/package.json +++ b/package.json @@ -39,6 +39,7 @@ "react-hook-form": "7.51.2", "react-native": "0.73.6", "react-native-calendars": "1.1304.1", + "react-native-circular-progress-indicator": "4.4.2", "react-native-elements": "3.4.3", "react-native-gesture-handler": "2.14.1", "react-native-paper": "5.12.3", diff --git a/presentation/react/components/Stats/Stats.tsx b/presentation/react/components/Stats/Stats.tsx index 00b00e1..4593790 100644 --- a/presentation/react/components/Stats/Stats.tsx +++ b/presentation/react/components/Stats/Stats.tsx @@ -1,35 +1,68 @@ import { SafeAreaView } from "react-native-safe-area-context" import { Card, Text } from "react-native-paper" +import CircularProgress from "react-native-circular-progress-indicator" +import { ScrollView } from "react-native" +import { Calendar } from "react-native-calendars" -export const Stats: React.FC = () => { +import type { HabitsTracker } from "@/domain/entities/HabitsTracker" + +export interface StatsProps { + habitsTracker: HabitsTracker +} + +export const Stats: React.FC = (props) => { + const { habitsTracker } = props + + const habitsHistory = habitsTracker.getAllHabitsHistory() return ( - {"Statistique"} + + - - - - nbDays Sucess that follow - - - - - - nbDays Sucess - - - - - - nbDays Fail - - - - - - CardContent - - + {habitsHistory.map((element) => { + if (element.habit.goal.frequency === "daily") { + return ( + + + + + nbDays Sucess dans la semaine + + + + + ) + } + if (element.habit.goal.frequency === "weekly") { + return ( + + + + nbDays Sucess dans le mois + + + ) + } + if (element.habit.goal.frequency === "monthly") { + return ( + + + + nbDays Sucess dans le mois + + + ) + } + return null + })} + ) } From e68fe6075e5210ee09cf29698286f71f0313bb68 Mon Sep 17 00:00:00 2001 From: Maxime RICHARD Date: Mon, 20 May 2024 15:35:49 +0200 Subject: [PATCH 3/6] feat: stats v1 --- presentation/react/components/Stats/Stats.tsx | 167 ++++++++++++++---- 1 file changed, 133 insertions(+), 34 deletions(-) diff --git a/presentation/react/components/Stats/Stats.tsx b/presentation/react/components/Stats/Stats.tsx index 4593790..3cbd071 100644 --- a/presentation/react/components/Stats/Stats.tsx +++ b/presentation/react/components/Stats/Stats.tsx @@ -1,9 +1,9 @@ -import { SafeAreaView } from "react-native-safe-area-context" import { Card, Text } from "react-native-paper" import CircularProgress from "react-native-circular-progress-indicator" -import { ScrollView } from "react-native" -import { Calendar } from "react-native-calendars" +import { Agenda } from "react-native-calendars" +import { useState } from "react" +import { getNowDate, getISODate } from "@/utils/dates" import type { HabitsTracker } from "@/domain/entities/HabitsTracker" export interface StatsProps { @@ -13,23 +13,107 @@ export interface StatsProps { export const Stats: React.FC = (props) => { const { habitsTracker } = props - const habitsHistory = habitsTracker.getAllHabitsHistory() - return ( - - - + const today = getNowDate() + const todayISO = getISODate(today) - {habitsHistory.map((element) => { - if (element.habit.goal.frequency === "daily") { - return ( - - + const [selectedDate, setSelectedDate] = useState(today) + const selectedDateISO = getISODate(selectedDate) + + const habitsHistory = habitsTracker.getAllHabitsHistory() + + let goalDays = 0 + let totalGoalDays = 0 + const dailyHabits = habitsHistory.filter((el) => { + return ( + el.habit.goal.frequency === "daily" && el.habit.startDate <= selectedDate + ) + }) + let displayDaily = true + if (dailyHabits.length === 0) { + displayDaily = false + } else { + for (const el of dailyHabits) { + totalGoalDays++ + if ( + el.getProgressesByDate(selectedDate)[0]?.goalProgress.isCompleted() ?? + false + ) { + goalDays++ + } + } + } + + let goalWeek = 0 + let totalGoalWeek = 0 + const weeklyHabits = habitsHistory.filter((el) => { + return ( + el.habit.goal.frequency === "weekly" && el.habit.startDate <= selectedDate + ) + }) + + let displayWeekly = true + if (weeklyHabits.length === 0) { + displayWeekly = false + } else { + for (const el of weeklyHabits) { + totalGoalWeek++ + if ( + el.getProgressesByDate(selectedDate)[0]?.goalProgress.isCompleted() ?? + false + ) { + goalWeek++ + } + } + } + + let goalMonth = 0 + let totalGoalMonth = 0 + const monthlyHabits = habitsHistory.filter((el) => { + return ( + el.habit.goal.frequency === "monthly" && + el.habit.startDate <= selectedDate + ) + }) + + let displayMonthly = true + if (monthlyHabits.length === 0) { + displayMonthly = false + } else { + for (const el of monthlyHabits) { + totalGoalMonth++ + if ( + el.getProgressesByDate(selectedDate)[0]?.goalProgress.isCompleted() ?? + false + ) { + goalMonth++ + } + } + } + + return ( + { + setSelectedDate(new Date(date.dateString)) + }} + markedDates={{ + [todayISO]: { marked: true, today: true }, + }} + maxDate={todayISO} + selected={selectedDateISO} + renderList={() => { + return ( + <> + {displayDaily ? ( + + - nbDays Sucess dans la semaine + {goalDays} but réussi dans la journée sur {totalGoalDays} = (props) => { /> - ) - } - if (element.habit.goal.frequency === "weekly") { - return ( - - + ) : null} + {displayWeekly ? ( + + - nbDays Sucess dans le mois + + {goalWeek} but réussi dans la semaine sur {totalGoalWeek} + + - ) - } - if (element.habit.goal.frequency === "monthly") { - return ( - + ) : null} + {displayMonthly ? ( + - nbDays Sucess dans le mois + + {goalMonth} but réussi dans le mois sur {totalGoalMonth} + + - ) - } - return null - })} - - + ) : null} + + ) + }} + /> ) } From 42f5623c92ff8d2950ff6e48d6058af208c22c1c Mon Sep 17 00:00:00 2001 From: Maxime RICHARD Date: Tue, 21 May 2024 23:35:04 +0200 Subject: [PATCH 4/6] feat: finish stas --- presentation/react/components/Stats/Stats.tsx | 279 ++++++++++-------- 1 file changed, 155 insertions(+), 124 deletions(-) diff --git a/presentation/react/components/Stats/Stats.tsx b/presentation/react/components/Stats/Stats.tsx index bf3d9f8..f1260c1 100644 --- a/presentation/react/components/Stats/Stats.tsx +++ b/presentation/react/components/Stats/Stats.tsx @@ -1,18 +1,18 @@ import { Card, Text } from "react-native-paper" import CircularProgress from "react-native-circular-progress-indicator" import { Agenda } from "react-native-calendars" -import { useState } from "react" +import type { SetStateAction } from "react" +import { useState, useEffect } from "react" import { getNowDateUTC, getISODate } from "@/utils/dates" import type { HabitsTracker } from "@/domain/entities/HabitsTracker" +import type { HabitHistory } from "@/domain/entities/HabitHistory" export interface StatsProps { habitsTracker: HabitsTracker } -export const Stats: React.FC = (props) => { - const { habitsTracker } = props - +export const Stats: React.FC = ({ habitsTracker }) => { const today = getNowDateUTC() const todayISO = getISODate(today) @@ -21,81 +21,98 @@ export const Stats: React.FC = (props) => { const habitsHistory = habitsTracker.getAllHabitsHistory() - let goalDays = 0 - let totalGoalDays = 0 - const dailyHabits = habitsHistory.filter((el) => { - return ( - el.habit.goal.frequency === "daily" && el.habit.startDate <= selectedDate - ) - }) - let displayDaily = true - if (dailyHabits.length === 0) { - displayDaily = false - } else { - for (const el of dailyHabits) { - totalGoalDays++ - if ( - el.getProgressesByDate(selectedDate)[0]?.goalProgress.isCompleted() ?? - false - ) { - goalDays++ - } + const [goalDays, setGoalDays] = useState(0) + const [totalGoalDays, setTotalGoalDays] = useState(0) + const [displayDaily, setDisplayDaily] = useState(true) + const [goalWeek, setGoalWeek] = useState(0) + const [totalGoalWeek, setTotalGoalWeek] = useState(0) + const [displayWeekly, setDisplayWeekly] = useState(true) + const [goalMonth, setGoalMonth] = useState(0) + const [totalGoalMonth, setTotalGoalMonth] = useState(0) + const [displayMonthly, setDisplayMonthly] = useState(true) + + const updateStats = (date: Date): void => { + const dailyHabits = habitsHistory.filter((el) => { + return ( + el.habit.goal.frequency === "daily" && + el.habit.startDate.getFullYear() <= date.getFullYear() && + el.habit.startDate.getMonth() <= date.getMonth() && + el.habit.startDate.getDate() <= date.getDate() + ) + }) + const weeklyHabits = habitsHistory.filter((el) => { + return ( + el.habit.goal.frequency === "weekly" && + el.habit.startDate.getFullYear() <= date.getFullYear() && + el.habit.startDate.getMonth() <= date.getMonth() && + el.habit.startDate.getDate() <= date.getDate() + ) + }) + const monthlyHabits = habitsHistory.filter((el) => { + return ( + el.habit.goal.frequency === "monthly" && + el.habit.startDate.getFullYear() <= date.getFullYear() && + el.habit.startDate.getMonth() <= date.getMonth() && + el.habit.startDate.getDate() <= date.getDate() + ) + }) + + const calculateGoals = ( + habits: HabitHistory[], + setTotalGoals: { + (value: SetStateAction): void + (value: SetStateAction): void + (value: SetStateAction): void + (arg0: any): void + }, + setGoals: { + (value: SetStateAction): void + (value: SetStateAction): void + (value: SetStateAction): void + (arg0: any): void + }, + ): void => { + setTotalGoals(habits.length) + const completedGoals = habits.filter((el) => { + return ( + el.getProgressesByDate(date)[0]?.goalProgress.isCompleted() ?? false + ) + }).length + setGoals(completedGoals) + } + + if (dailyHabits.length === 0) { + setDisplayDaily(false) + } else { + setDisplayDaily(true) + calculateGoals(dailyHabits, setTotalGoalDays, setGoalDays) + } + + if (weeklyHabits.length === 0) { + setDisplayWeekly(false) + } else { + setDisplayWeekly(true) + calculateGoals(weeklyHabits, setTotalGoalWeek, setGoalWeek) + } + + if (monthlyHabits.length === 0) { + setDisplayMonthly(false) + } else { + setDisplayMonthly(true) + calculateGoals(monthlyHabits, setTotalGoalMonth, setGoalMonth) } } - let goalWeek = 0 - let totalGoalWeek = 0 - const weeklyHabits = habitsHistory.filter((el) => { - return ( - el.habit.goal.frequency === "weekly" && el.habit.startDate <= selectedDate - ) - }) - - let displayWeekly = true - if (weeklyHabits.length === 0) { - displayWeekly = false - } else { - for (const el of weeklyHabits) { - totalGoalWeek++ - if ( - el.getProgressesByDate(selectedDate)[0]?.goalProgress.isCompleted() ?? - false - ) { - goalWeek++ - } - } - } - - let goalMonth = 0 - let totalGoalMonth = 0 - const monthlyHabits = habitsHistory.filter((el) => { - return ( - el.habit.goal.frequency === "monthly" && - el.habit.startDate <= selectedDate - ) - }) - - let displayMonthly = true - if (monthlyHabits.length === 0) { - displayMonthly = false - } else { - for (const el of monthlyHabits) { - totalGoalMonth++ - if ( - el.getProgressesByDate(selectedDate)[0]?.goalProgress.isCompleted() ?? - false - ) { - goalMonth++ - } - } - } + useEffect(() => { + updateStats(selectedDate) + }, [selectedDate]) return ( { - setSelectedDate(new Date(date.dateString)) + return setSelectedDate(new Date(date.dateString)) }} markedDates={{ [todayISO]: { marked: true, today: true }, @@ -105,60 +122,74 @@ export const Stats: React.FC = (props) => { renderList={() => { return ( <> - {displayDaily ? ( - - - - - {goalDays} but réussi dans la journée sur {totalGoalDays} - - - - - ) : null} - {displayWeekly ? ( - - - - - {goalWeek} but réussi dans la semaine sur {totalGoalWeek} - - - - - ) : null} - {displayMonthly ? ( - - - - - {goalMonth} but réussi dans le mois sur {totalGoalMonth} - - - - - ) : null} + + + + {displayDaily ? ( + <> + + {goalDays} but réussi dans la journée sur {totalGoalDays} + + + + ) : ( + Aucun objectif quotidien + )} + + + + + + + {displayWeekly ? ( + <> + + {goalWeek} but réussi dans la semaine sur {totalGoalWeek} + + + + ) : ( + Aucun objectif hebdomadaire + )} + + + + + + + {displayMonthly ? ( + <> + + {goalMonth} but réussi dans le mois sur {totalGoalMonth} + + + + ) : ( + Aucun objectif mensuel + )} + + ) }} From ca122e9fce3b17ce7f5ed2ede6c5f372aca4167f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Th=C3=A9o=20LUDWIG?= Date: Wed, 22 May 2024 12:46:51 +0200 Subject: [PATCH 5/6] Revert "feat: finish stas" This reverts commit 42f5623c92ff8d2950ff6e48d6058af208c22c1c. --- presentation/react/components/Stats/Stats.tsx | 279 ++++++++---------- 1 file changed, 124 insertions(+), 155 deletions(-) diff --git a/presentation/react/components/Stats/Stats.tsx b/presentation/react/components/Stats/Stats.tsx index f1260c1..bf3d9f8 100644 --- a/presentation/react/components/Stats/Stats.tsx +++ b/presentation/react/components/Stats/Stats.tsx @@ -1,18 +1,18 @@ import { Card, Text } from "react-native-paper" import CircularProgress from "react-native-circular-progress-indicator" import { Agenda } from "react-native-calendars" -import type { SetStateAction } from "react" -import { useState, useEffect } from "react" +import { useState } from "react" import { getNowDateUTC, getISODate } from "@/utils/dates" import type { HabitsTracker } from "@/domain/entities/HabitsTracker" -import type { HabitHistory } from "@/domain/entities/HabitHistory" export interface StatsProps { habitsTracker: HabitsTracker } -export const Stats: React.FC = ({ habitsTracker }) => { +export const Stats: React.FC = (props) => { + const { habitsTracker } = props + const today = getNowDateUTC() const todayISO = getISODate(today) @@ -21,98 +21,81 @@ export const Stats: React.FC = ({ habitsTracker }) => { const habitsHistory = habitsTracker.getAllHabitsHistory() - const [goalDays, setGoalDays] = useState(0) - const [totalGoalDays, setTotalGoalDays] = useState(0) - const [displayDaily, setDisplayDaily] = useState(true) - const [goalWeek, setGoalWeek] = useState(0) - const [totalGoalWeek, setTotalGoalWeek] = useState(0) - const [displayWeekly, setDisplayWeekly] = useState(true) - const [goalMonth, setGoalMonth] = useState(0) - const [totalGoalMonth, setTotalGoalMonth] = useState(0) - const [displayMonthly, setDisplayMonthly] = useState(true) - - const updateStats = (date: Date): void => { - const dailyHabits = habitsHistory.filter((el) => { - return ( - el.habit.goal.frequency === "daily" && - el.habit.startDate.getFullYear() <= date.getFullYear() && - el.habit.startDate.getMonth() <= date.getMonth() && - el.habit.startDate.getDate() <= date.getDate() - ) - }) - const weeklyHabits = habitsHistory.filter((el) => { - return ( - el.habit.goal.frequency === "weekly" && - el.habit.startDate.getFullYear() <= date.getFullYear() && - el.habit.startDate.getMonth() <= date.getMonth() && - el.habit.startDate.getDate() <= date.getDate() - ) - }) - const monthlyHabits = habitsHistory.filter((el) => { - return ( - el.habit.goal.frequency === "monthly" && - el.habit.startDate.getFullYear() <= date.getFullYear() && - el.habit.startDate.getMonth() <= date.getMonth() && - el.habit.startDate.getDate() <= date.getDate() - ) - }) - - const calculateGoals = ( - habits: HabitHistory[], - setTotalGoals: { - (value: SetStateAction): void - (value: SetStateAction): void - (value: SetStateAction): void - (arg0: any): void - }, - setGoals: { - (value: SetStateAction): void - (value: SetStateAction): void - (value: SetStateAction): void - (arg0: any): void - }, - ): void => { - setTotalGoals(habits.length) - const completedGoals = habits.filter((el) => { - return ( - el.getProgressesByDate(date)[0]?.goalProgress.isCompleted() ?? false - ) - }).length - setGoals(completedGoals) - } - - if (dailyHabits.length === 0) { - setDisplayDaily(false) - } else { - setDisplayDaily(true) - calculateGoals(dailyHabits, setTotalGoalDays, setGoalDays) - } - - if (weeklyHabits.length === 0) { - setDisplayWeekly(false) - } else { - setDisplayWeekly(true) - calculateGoals(weeklyHabits, setTotalGoalWeek, setGoalWeek) - } - - if (monthlyHabits.length === 0) { - setDisplayMonthly(false) - } else { - setDisplayMonthly(true) - calculateGoals(monthlyHabits, setTotalGoalMonth, setGoalMonth) + let goalDays = 0 + let totalGoalDays = 0 + const dailyHabits = habitsHistory.filter((el) => { + return ( + el.habit.goal.frequency === "daily" && el.habit.startDate <= selectedDate + ) + }) + let displayDaily = true + if (dailyHabits.length === 0) { + displayDaily = false + } else { + for (const el of dailyHabits) { + totalGoalDays++ + if ( + el.getProgressesByDate(selectedDate)[0]?.goalProgress.isCompleted() ?? + false + ) { + goalDays++ + } } } - useEffect(() => { - updateStats(selectedDate) - }, [selectedDate]) + let goalWeek = 0 + let totalGoalWeek = 0 + const weeklyHabits = habitsHistory.filter((el) => { + return ( + el.habit.goal.frequency === "weekly" && el.habit.startDate <= selectedDate + ) + }) + + let displayWeekly = true + if (weeklyHabits.length === 0) { + displayWeekly = false + } else { + for (const el of weeklyHabits) { + totalGoalWeek++ + if ( + el.getProgressesByDate(selectedDate)[0]?.goalProgress.isCompleted() ?? + false + ) { + goalWeek++ + } + } + } + + let goalMonth = 0 + let totalGoalMonth = 0 + const monthlyHabits = habitsHistory.filter((el) => { + return ( + el.habit.goal.frequency === "monthly" && + el.habit.startDate <= selectedDate + ) + }) + + let displayMonthly = true + if (monthlyHabits.length === 0) { + displayMonthly = false + } else { + for (const el of monthlyHabits) { + totalGoalMonth++ + if ( + el.getProgressesByDate(selectedDate)[0]?.goalProgress.isCompleted() ?? + false + ) { + goalMonth++ + } + } + } return ( { - return setSelectedDate(new Date(date.dateString)) + setSelectedDate(new Date(date.dateString)) }} markedDates={{ [todayISO]: { marked: true, today: true }, @@ -122,74 +105,60 @@ export const Stats: React.FC = ({ habitsTracker }) => { renderList={() => { return ( <> - - - - {displayDaily ? ( - <> - - {goalDays} but réussi dans la journée sur {totalGoalDays} - - - - ) : ( - Aucun objectif quotidien - )} - - - - - - - {displayWeekly ? ( - <> - - {goalWeek} but réussi dans la semaine sur {totalGoalWeek} - - - - ) : ( - Aucun objectif hebdomadaire - )} - - - - - - - {displayMonthly ? ( - <> - - {goalMonth} but réussi dans le mois sur {totalGoalMonth} - - - - ) : ( - Aucun objectif mensuel - )} - - + {displayDaily ? ( + + + + + {goalDays} but réussi dans la journée sur {totalGoalDays} + + + + + ) : null} + {displayWeekly ? ( + + + + + {goalWeek} but réussi dans la semaine sur {totalGoalWeek} + + + + + ) : null} + {displayMonthly ? ( + + + + + {goalMonth} but réussi dans le mois sur {totalGoalMonth} + + + + + ) : null} ) }} From e15a3982fd6c6afb9cddd4e297976d1f263ca5d3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Th=C3=A9o=20LUDWIG?= Date: Wed, 22 May 2024 13:44:54 +0200 Subject: [PATCH 6/6] fix: habits statistics --- .eslintrc.json | 9 +- app/application/_layout.tsx | 2 +- app/application/habits/statistics.tsx | 23 + app/application/habits/stats.tsx | 10 - domain/entities/Goal.ts | 6 + domain/entities/HabitsTracker.ts | 25 + package-lock.json | 539 +++++++++++++----- package.json | 14 +- .../components/HabitsMainPage/HabitsList.tsx | 4 +- .../components/HabitsStatistics.tsx | 110 ++++ presentation/react/components/Stats/Stats.tsx | 167 ------ utils/__tests__/maths.test.ts | 83 +++ utils/maths.ts | 9 + utils/strings.ts | 2 + 14 files changed, 683 insertions(+), 320 deletions(-) create mode 100644 app/application/habits/statistics.tsx delete mode 100644 app/application/habits/stats.tsx create mode 100644 presentation/react-native/components/HabitsStatistics.tsx delete mode 100644 presentation/react/components/Stats/Stats.tsx create mode 100644 utils/__tests__/maths.test.ts create mode 100644 utils/maths.ts diff --git a/.eslintrc.json b/.eslintrc.json index 743a7e9..9de8abe 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -1,4 +1,5 @@ { + "root": true, "extends": [ "conventions", "plugin:react/recommended", @@ -15,9 +16,6 @@ "version": "detect" } }, - "parserOptions": { - "project": "./tsconfig.json" - }, "rules": { "react/react-in-jsx-scope": "off", "react/prop-types": "off", @@ -35,8 +33,9 @@ { "files": ["*.ts", "*.tsx"], "parser": "@typescript-eslint/parser", - "rules": { - "@typescript-eslint/member-delimiter-style": "off" + "plugins": ["@typescript-eslint"], + "parserOptions": { + "project": "./tsconfig.json" } } ] diff --git a/app/application/_layout.tsx b/app/application/_layout.tsx index 060d4bb..03215f6 100644 --- a/app/application/_layout.tsx +++ b/app/application/_layout.tsx @@ -45,7 +45,7 @@ const TabLayout: React.FC = () => { }} /> { diff --git a/app/application/habits/statistics.tsx b/app/application/habits/statistics.tsx new file mode 100644 index 0000000..c9fb2ac --- /dev/null +++ b/app/application/habits/statistics.tsx @@ -0,0 +1,23 @@ +import { SafeAreaView } from "react-native-safe-area-context" + +import { HabitsStatistics } from "@/presentation/react-native/components/HabitsStatistics" +import { useHabitsTracker } from "@/presentation/react/contexts/HabitsTracker" + +const StatisticsPage: React.FC = () => { + const { habitsTracker } = useHabitsTracker() + + return ( + + + + ) +} + +export default StatisticsPage diff --git a/app/application/habits/stats.tsx b/app/application/habits/stats.tsx deleted file mode 100644 index 73a642a..0000000 --- a/app/application/habits/stats.tsx +++ /dev/null @@ -1,10 +0,0 @@ -import { Stats } from "@/presentation/react/components/Stats/Stats" -import { useHabitsTracker } from "@/presentation/react/contexts/HabitsTracker" - -const StatsPage: React.FC = () => { - const { habitsTracker } = useHabitsTracker() - - return -} - -export default StatsPage diff --git a/domain/entities/Goal.ts b/domain/entities/Goal.ts index e3c4077..371314e 100644 --- a/domain/entities/Goal.ts +++ b/domain/entities/Goal.ts @@ -11,6 +11,12 @@ export const GOAL_FREQUENCIES = GOAL_FREQUENCIES_ZOD.map((frequency) => { }) export type GoalFrequency = (typeof GOAL_FREQUENCIES)[number] +export const GOAL_FREQUENCIES_TYPES = { + daily: "day", + weekly: "week", + monthly: "month", +} as const + export const GOAL_TYPES_ZOD = [ z.literal("boolean"), z.literal("numeric"), diff --git a/domain/entities/HabitsTracker.ts b/domain/entities/HabitsTracker.ts index e7e62d9..4b601e8 100644 --- a/domain/entities/HabitsTracker.ts +++ b/domain/entities/HabitsTracker.ts @@ -96,4 +96,29 @@ export class HabitsTracker implements HabitsTrackerData { ) }) } + + public getHabitsStatisticsByDateAndFrequency({ + selectedDate, + frequency, + }: { + selectedDate: Date + frequency: GoalFrequency + }): { + totalGoalsSuccess: number + totalGoals: number + } { + const habitsHistory = this.getHabitsHistoriesByDate({ + selectedDate, + frequency, + }) + let totalGoalsSuccess = 0 + const totalGoals = habitsHistory.length + for (const habitHistory of habitsHistory) { + const goalProgress = habitHistory.getGoalProgressByDate(selectedDate) + if (goalProgress.isCompleted()) { + totalGoalsSuccess++ + } + } + return { totalGoalsSuccess, totalGoals } + } } diff --git a/package-lock.json b/package-lock.json index 68dab97..ddbdb4f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,7 +13,7 @@ "@fortawesome/fontawesome-svg-core": "6.5.2", "@fortawesome/free-solid-svg-icons": "6.5.2", "@fortawesome/react-native-fontawesome": "0.3.1", - "@hookform/resolvers": "3.4.0", + "@hookform/resolvers": "3.4.2", "@react-native-async-storage/async-storage": "1.23.1", "@react-navigation/native": "6.1.17", "@supabase/supabase-js": "2.43.2", @@ -28,7 +28,7 @@ "lottie-react-native": "6.7.0", "react": "18.2.0", "react-dom": "18.2.0", - "react-hook-form": "7.51.4", + "react-hook-form": "7.51.5", "react-native": "0.74.1", "react-native-calendars": "1.1305.0", "react-native-circular-progress-indicator": "4.4.2", @@ -57,21 +57,21 @@ "@types/node": "20.12.12", "@types/react": "18.2.79", "@types/react-test-renderer": "18.3.0", - "@typescript-eslint/eslint-plugin": "7.9.0", - "@typescript-eslint/parser": "7.9.0", + "@typescript-eslint/eslint-plugin": "7.10.0", + "@typescript-eslint/parser": "7.10.0", "eslint": "8.57.0", - "eslint-config-conventions": "14.1.0", + "eslint-config-conventions": "14.2.0", "eslint-plugin-import": "2.29.1", "eslint-plugin-promise": "6.1.1", "eslint-plugin-react": "7.34.1", "eslint-plugin-react-hooks": "4.6.2", "eslint-plugin-react-native": "4.1.0", - "eslint-plugin-unicorn": "51.0.1", + "eslint-plugin-unicorn": "53.0.0", "husky": "9.0.11", "jest": "29.7.0", "jest-expo": "51.0.2", "jest-junit": "16.0.0", - "lint-staged": "15.2.2", + "lint-staged": "15.2.4", "prettier": "3.2.5", "react-test-renderer": "18.2.0", "supabase": "1.167.4", @@ -4362,9 +4362,9 @@ } }, "node_modules/@hookform/resolvers": { - "version": "3.4.0", - "resolved": "https://registry.npmjs.org/@hookform/resolvers/-/resolvers-3.4.0.tgz", - "integrity": "sha512-+oAqK3okmoEDnvUkJ3N/mvNMeeMv5Apgy1jkoRmlaaAF4vBgcJs9tHvtXU7VE4DvPosvAUUkPOaNFunzt1dbgA==", + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/@hookform/resolvers/-/resolvers-3.4.2.tgz", + "integrity": "sha512-1m9uAVIO8wVf7VCDAGsuGA0t6Z3m6jVGAN50HkV9vYLl0yixKK/Z1lr01vaRvYCkIKGoy1noVRxMzQYb4y/j1Q==", "license": "MIT", "peerDependencies": { "react-hook-form": "^7.0.0" @@ -8751,9 +8751,9 @@ } }, "node_modules/@types/babel__traverse": { - "version": "7.20.5", - "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.20.5.tgz", - "integrity": "sha512-WXCyOcRtH37HAUkpXhUduaxdm82b4GSlyTqajXviN4EfiuPgNYR109xMCKvpl6zPIpua0DGlMEDCq+g8EdoheQ==", + "version": "7.20.6", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.20.6.tgz", + "integrity": "sha512-r1bzfrm0tomOI8g1SzvCaQHo6Lcv6zu0EA+W2kHrt8dyrHQxGzBBL4kdkzIS+jBMV+EYcMAEAqXqYaLJq5rOZg==", "dev": true, "license": "MIT", "dependencies": { @@ -8966,17 +8966,17 @@ "license": "MIT" }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "7.9.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.9.0.tgz", - "integrity": "sha512-6e+X0X3sFe/G/54aC3jt0txuMTURqLyekmEHViqyA2VnxhLMpvA6nqmcjIy+Cr9tLDHPssA74BP5Mx9HQIxBEA==", + "version": "7.10.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.10.0.tgz", + "integrity": "sha512-PzCr+a/KAef5ZawX7nbyNwBDtM1HdLIT53aSA2DDlxmxMngZ43O8SIePOeX8H5S+FHXeI6t97mTt/dDdzY4Fyw==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/regexpp": "^4.10.0", - "@typescript-eslint/scope-manager": "7.9.0", - "@typescript-eslint/type-utils": "7.9.0", - "@typescript-eslint/utils": "7.9.0", - "@typescript-eslint/visitor-keys": "7.9.0", + "@typescript-eslint/scope-manager": "7.10.0", + "@typescript-eslint/type-utils": "7.10.0", + "@typescript-eslint/utils": "7.10.0", + "@typescript-eslint/visitor-keys": "7.10.0", "graphemer": "^1.4.0", "ignore": "^5.3.1", "natural-compare": "^1.4.0", @@ -9000,16 +9000,16 @@ } }, "node_modules/@typescript-eslint/parser": { - "version": "7.9.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-7.9.0.tgz", - "integrity": "sha512-qHMJfkL5qvgQB2aLvhUSXxbK7OLnDkwPzFalg458pxQgfxKDfT1ZDbHQM/I6mDIf/svlMkj21kzKuQ2ixJlatQ==", + "version": "7.10.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-7.10.0.tgz", + "integrity": "sha512-2EjZMA0LUW5V5tGQiaa2Gys+nKdfrn2xiTIBLR4fxmPmVSvgPcKNW+AE/ln9k0A4zDUti0J/GZXMDupQoI+e1w==", "dev": true, "license": "BSD-2-Clause", "dependencies": { - "@typescript-eslint/scope-manager": "7.9.0", - "@typescript-eslint/types": "7.9.0", - "@typescript-eslint/typescript-estree": "7.9.0", - "@typescript-eslint/visitor-keys": "7.9.0", + "@typescript-eslint/scope-manager": "7.10.0", + "@typescript-eslint/types": "7.10.0", + "@typescript-eslint/typescript-estree": "7.10.0", + "@typescript-eslint/visitor-keys": "7.10.0", "debug": "^4.3.4" }, "engines": { @@ -9029,14 +9029,14 @@ } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "7.9.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-7.9.0.tgz", - "integrity": "sha512-ZwPK4DeCDxr3GJltRz5iZejPFAAr4Wk3+2WIBaj1L5PYK5RgxExu/Y68FFVclN0y6GGwH8q+KgKRCvaTmFBbgQ==", + "version": "7.10.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-7.10.0.tgz", + "integrity": "sha512-7L01/K8W/VGl7noe2mgH0K7BE29Sq6KAbVmxurj8GGaPDZXPr8EEQ2seOeAS+mEV9DnzxBQB6ax6qQQ5C6P4xg==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "7.9.0", - "@typescript-eslint/visitor-keys": "7.9.0" + "@typescript-eslint/types": "7.10.0", + "@typescript-eslint/visitor-keys": "7.10.0" }, "engines": { "node": "^18.18.0 || >=20.0.0" @@ -9047,14 +9047,14 @@ } }, "node_modules/@typescript-eslint/type-utils": { - "version": "7.9.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-7.9.0.tgz", - "integrity": "sha512-6Qy8dfut0PFrFRAZsGzuLoM4hre4gjzWJB6sUvdunCYZsYemTkzZNwF1rnGea326PHPT3zn5Lmg32M/xfJfByA==", + "version": "7.10.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-7.10.0.tgz", + "integrity": "sha512-D7tS4WDkJWrVkuzgm90qYw9RdgBcrWmbbRkrLA4d7Pg3w0ttVGDsvYGV19SH8gPR5L7OtcN5J1hTtyenO9xE9g==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/typescript-estree": "7.9.0", - "@typescript-eslint/utils": "7.9.0", + "@typescript-eslint/typescript-estree": "7.10.0", + "@typescript-eslint/utils": "7.10.0", "debug": "^4.3.4", "ts-api-utils": "^1.3.0" }, @@ -9075,9 +9075,9 @@ } }, "node_modules/@typescript-eslint/types": { - "version": "7.9.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.9.0.tgz", - "integrity": "sha512-oZQD9HEWQanl9UfsbGVcZ2cGaR0YT5476xfWE0oE5kQa2sNK2frxOlkeacLOTh9po4AlUT5rtkGyYM5kew0z5w==", + "version": "7.10.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.10.0.tgz", + "integrity": "sha512-7fNj+Ya35aNyhuqrA1E/VayQX9Elwr8NKZ4WueClR3KwJ7Xx9jcCdOrLW04h51de/+gNbyFMs+IDxh5xIwfbNg==", "dev": true, "license": "MIT", "engines": { @@ -9089,14 +9089,14 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "7.9.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-7.9.0.tgz", - "integrity": "sha512-zBCMCkrb2YjpKV3LA0ZJubtKCDxLttxfdGmwZvTqqWevUPN0FZvSI26FalGFFUZU/9YQK/A4xcQF9o/VVaCKAg==", + "version": "7.10.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-7.10.0.tgz", + "integrity": "sha512-LXFnQJjL9XIcxeVfqmNj60YhatpRLt6UhdlFwAkjNc6jSUlK8zQOl1oktAP8PlWFzPQC1jny/8Bai3/HPuvN5g==", "dev": true, "license": "BSD-2-Clause", "dependencies": { - "@typescript-eslint/types": "7.9.0", - "@typescript-eslint/visitor-keys": "7.9.0", + "@typescript-eslint/types": "7.10.0", + "@typescript-eslint/visitor-keys": "7.10.0", "debug": "^4.3.4", "globby": "^11.1.0", "is-glob": "^4.0.3", @@ -9131,16 +9131,16 @@ } }, "node_modules/@typescript-eslint/utils": { - "version": "7.9.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-7.9.0.tgz", - "integrity": "sha512-5KVRQCzZajmT4Ep+NEgjXCvjuypVvYHUW7RHlXzNPuak2oWpVoD1jf5xCP0dPAuNIchjC7uQyvbdaSTFaLqSdA==", + "version": "7.10.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-7.10.0.tgz", + "integrity": "sha512-olzif1Fuo8R8m/qKkzJqT7qwy16CzPRWBvERS0uvyc+DHd8AKbO4Jb7kpAvVzMmZm8TrHnI7hvjN4I05zow+tg==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.4.0", - "@typescript-eslint/scope-manager": "7.9.0", - "@typescript-eslint/types": "7.9.0", - "@typescript-eslint/typescript-estree": "7.9.0" + "@typescript-eslint/scope-manager": "7.10.0", + "@typescript-eslint/types": "7.10.0", + "@typescript-eslint/typescript-estree": "7.10.0" }, "engines": { "node": "^18.18.0 || >=20.0.0" @@ -9154,13 +9154,13 @@ } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "7.9.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.9.0.tgz", - "integrity": "sha512-iESPx2TNLDNGQLyjKhUvIKprlP49XNEK+MvIf9nIO7ZZaZdbnfWKHnXAgufpxqfA0YryH8XToi4+CjBgVnFTSQ==", + "version": "7.10.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.10.0.tgz", + "integrity": "sha512-9ntIVgsi6gg6FIq9xjEO4VQJvwOqA3jaBFQJ/6TK5AvEup2+cECI6Fh7QiBxmfMHXU0V0J4RyPeOU1VDNzl9cg==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "7.9.0", + "@typescript-eslint/types": "7.10.0", "eslint-visitor-keys": "^3.4.3" }, "engines": { @@ -9978,9 +9978,9 @@ } }, "node_modules/babel-plugin-react-native-web": { - "version": "0.19.11", - "resolved": "https://registry.npmjs.org/babel-plugin-react-native-web/-/babel-plugin-react-native-web-0.19.11.tgz", - "integrity": "sha512-0sHf8GgDhsRZxGwlwHHdfL3U8wImFaLw4haEa60U9M3EiO3bg6u3BJ+1vXhwgrevqSq76rMb5j1HJs+dNvMj5g==", + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/babel-plugin-react-native-web/-/babel-plugin-react-native-web-0.19.12.tgz", + "integrity": "sha512-eYZ4+P6jNcB37lObWIg0pUbi7+3PKoU1Oie2j0C8UF3cXyXoR74tO2NBjI/FORb2LJyItJZEAmjU5pSaJYEL1w==", "license": "MIT" }, "node_modules/babel-plugin-transform-flow-enums": { @@ -10189,12 +10189,12 @@ } }, "node_modules/braces": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", - "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", "license": "MIT", "dependencies": { - "fill-range": "^7.0.1" + "fill-range": "^7.1.1" }, "engines": { "node": ">=8" @@ -10345,13 +10345,13 @@ } }, "node_modules/cacache/node_modules/glob": { - "version": "10.3.15", - "resolved": "https://registry.npmjs.org/glob/-/glob-10.3.15.tgz", - "integrity": "sha512-0c6RlJt1TICLyvJYIApxb8GsXoai0KUP7AxKKAtsYXdgJR1mGEUa7DgwShbdk1nly0PYoZj01xd4hzbq3fsjpw==", + "version": "10.3.16", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.3.16.tgz", + "integrity": "sha512-JDKXl1DiuuHJ6fVS2FXjownaavciiHNUU4mOvV/B793RLh05vZL1rcPnCSaOgv1hDT6RDlY7AB7ZUvFYAtPgAw==", "license": "ISC", "dependencies": { "foreground-child": "^3.1.0", - "jackspeak": "^2.3.6", + "jackspeak": "^3.1.2", "minimatch": "^9.0.1", "minipass": "^7.0.4", "path-scurry": "^1.11.0" @@ -10446,9 +10446,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001620", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001620.tgz", - "integrity": "sha512-WJvYsOjd1/BYUY6SNGUosK9DUidBPDTnOARHp3fSmFO1ekdxaY6nKRttEVrfMmYi80ctS0kz1wiWmm14fVc3ew==", + "version": "1.0.30001621", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001621.tgz", + "integrity": "sha512-+NLXZiviFFKX0fk8Piwv3PfLPGtRqJeq2TiNoUff/qB5KJgwecJTvCXDpmlyP/eCI/GUEmp/h/y5j0yckiiZrA==", "funding": [ { "type": "opencollective", @@ -11992,9 +11992,9 @@ "license": "MIT" }, "node_modules/electron-to-chromium": { - "version": "1.4.774", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.774.tgz", - "integrity": "sha512-132O1XCd7zcTkzS3FgkAzKmnBuNJjK8WjcTtNuoylj7MYbqw5eXehjQ5OK91g0zm7OTKIPeaAG4CPoRfD9M1Mg==", + "version": "1.4.777", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.777.tgz", + "integrity": "sha512-n02NCwLJ3wexLfK/yQeqfywCblZqLcXphzmid5e8yVPdtEcida7li0A5WQKghHNG0FeOMCzeFOzEbtAh5riXFw==", "license": "ISC" }, "node_modules/emittery": { @@ -12392,20 +12392,19 @@ } }, "node_modules/eslint-config-conventions": { - "version": "14.1.0", - "resolved": "https://registry.npmjs.org/eslint-config-conventions/-/eslint-config-conventions-14.1.0.tgz", - "integrity": "sha512-oJOQcUfXD874XuJOCtoRmjuVKAJ+6TG92So0VDgwvsBx3R2pVAc34CmuvAhqamFeaNOkLTSFu/mVrnmZggzgIw==", + "version": "14.2.0", + "resolved": "https://registry.npmjs.org/eslint-config-conventions/-/eslint-config-conventions-14.2.0.tgz", + "integrity": "sha512-f6BY4d5ibQfNY4MYRN5jat/bCZW/m/6lRXAca5cc3Q84DnTYwimby1GutBvnKfiA+/ZhIUcgqbFZozLOldBIhQ==", "dev": true, "license": "MIT", "engines": { - "node": ">=18.0.0", - "npm": ">=10.0.0" + "node": ">=18.0.0" }, "peerDependencies": { "eslint": "^8.56.0", "eslint-plugin-import": "^2.29.1", "eslint-plugin-promise": "^6.1.1", - "eslint-plugin-unicorn": "^51.0.1" + "eslint-plugin-unicorn": "^51.0.1 || ^52.0.0 || ^53.0.0" } }, "node_modules/eslint-import-resolver-node": { @@ -12672,18 +12671,18 @@ } }, "node_modules/eslint-plugin-unicorn": { - "version": "51.0.1", - "resolved": "https://registry.npmjs.org/eslint-plugin-unicorn/-/eslint-plugin-unicorn-51.0.1.tgz", - "integrity": "sha512-MuR/+9VuB0fydoI0nIn2RDA5WISRn4AsJyNSaNKLVwie9/ONvQhxOBbkfSICBPnzKrB77Fh6CZZXjgTt/4Latw==", + "version": "53.0.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-unicorn/-/eslint-plugin-unicorn-53.0.0.tgz", + "integrity": "sha512-kuTcNo9IwwUCfyHGwQFOK/HjJAYzbODHN3wP0PgqbW+jbXqpNWxNVpVhj2tO9SixBwuAdmal8rVcWKBxwFnGuw==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-validator-identifier": "^7.22.20", + "@babel/helper-validator-identifier": "^7.24.5", "@eslint-community/eslint-utils": "^4.4.0", - "@eslint/eslintrc": "^2.1.4", + "@eslint/eslintrc": "^3.0.2", "ci-info": "^4.0.0", "clean-regexp": "^1.0.0", - "core-js-compat": "^3.34.0", + "core-js-compat": "^3.37.0", "esquery": "^1.5.0", "indent-string": "^4.0.0", "is-builtin-module": "^3.2.1", @@ -12692,11 +12691,11 @@ "read-pkg-up": "^7.0.1", "regexp-tree": "^0.1.27", "regjsparser": "^0.10.0", - "semver": "^7.5.4", + "semver": "^7.6.1", "strip-indent": "^3.0.0" }, "engines": { - "node": ">=16" + "node": ">=18.18" }, "funding": { "url": "https://github.com/sindresorhus/eslint-plugin-unicorn?sponsor=1" @@ -12705,6 +12704,102 @@ "eslint": ">=8.56.0" } }, + "node_modules/eslint-plugin-unicorn/node_modules/@eslint/eslintrc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.1.0.tgz", + "integrity": "sha512-4Bfj15dVJdoy3RfZmmo86RK1Fwzn6SstsvK9JS+BaVKqC6QQQQyXekNaC+g+LKNgkQ+2VhGAzm6hO40AhMR3zQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^10.0.1", + "globals": "^14.0.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.0", + "minimatch": "^3.1.2", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-plugin-unicorn/node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/eslint-plugin-unicorn/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/eslint-plugin-unicorn/node_modules/eslint-visitor-keys": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.0.0.tgz", + "integrity": "sha512-OtIRv/2GyiF6o/d8K7MYKKbXrOUBIK6SfkIRM4Z0dY3w+LiQ0vy3F57m0Z71bjbyeiWFiHJ8brqnmE6H6/jEuw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-plugin-unicorn/node_modules/espree": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/espree/-/espree-10.0.1.tgz", + "integrity": "sha512-MWkrWZbJsL2UwnjxTX3gG8FneachS/Mwg7tdGXce011sJd5b0JG54vat5KHnfSBODZ3Wvzd2WnjxyzsRoVv+ww==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.11.3", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^4.0.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-plugin-unicorn/node_modules/globals": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", + "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/eslint-plugin-unicorn/node_modules/jsesc": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.0.2.tgz", @@ -12718,6 +12813,26 @@ "node": ">=6" } }, + "node_modules/eslint-plugin-unicorn/node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/eslint-plugin-unicorn/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, "node_modules/eslint-plugin-unicorn/node_modules/semver": { "version": "7.6.2", "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.2.tgz", @@ -13725,9 +13840,9 @@ } }, "node_modules/fill-range": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", - "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", "license": "MIT", "dependencies": { "to-regex-range": "^5.0.1" @@ -14075,6 +14190,20 @@ "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", "license": "ISC" }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, "node_modules/function-bind": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", @@ -15638,9 +15767,9 @@ } }, "node_modules/jackspeak": { - "version": "2.3.6", - "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-2.3.6.tgz", - "integrity": "sha512-N3yCS/NegsOBokc8GAdM8UcmfsKiSS8cipheD/nivzr700H+nsMOxJjQnvwOcRYVuFkdH0wGUvW2WbXGmrZGbQ==", + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.1.2.tgz", + "integrity": "sha512-kWmLKn2tRtfYMF/BakihVVRzBKOxz4gJMiL2Rj91WnAB5TPZumSH99R/Yf1qE1u4uRimvCSJfm6hnxohXeEXjQ==", "license": "BlueOak-1.0.0", "dependencies": { "@isaacs/cliui": "^8.0.2" @@ -18378,6 +18507,106 @@ "lightningcss-win32-x64-msvc": "1.19.0" } }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.19.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.19.0.tgz", + "integrity": "sha512-wIJmFtYX0rXHsXHSr4+sC5clwblEMji7HHQ4Ub1/CznVRxtCFha6JIt5JZaNf8vQrfdZnBxLLC6R8pC818jXqg==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.19.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.19.0.tgz", + "integrity": "sha512-Lif1wD6P4poaw9c/4Uh2z+gmrWhw/HtXFoeZ3bEsv6Ia4tt8rOJBdkfVaUJ6VXmpKHALve+iTyP2+50xY1wKPw==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.19.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.19.0.tgz", + "integrity": "sha512-P15VXY5682mTXaiDtbnLYQflc8BYb774j2R84FgDLJTN6Qp0ZjWEFyN1SPqyfTj2B2TFjRHRUvQSSZ7qN4Weig==", + "cpu": [ + "arm" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.19.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.19.0.tgz", + "integrity": "sha512-zwXRjWqpev8wqO0sv0M1aM1PpjHz6RVIsBcxKszIG83Befuh4yNysjgHVplF9RTU7eozGe3Ts7r6we1+Qkqsww==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.19.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.19.0.tgz", + "integrity": "sha512-vSCKO7SDnZaFN9zEloKSZM5/kC5gbzUjoJQ43BvUpyTFUX7ACs/mDfl2Eq6fdz2+uWhUh7vf92c4EaaP4udEtA==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, "node_modules/lightningcss-linux-x64-gnu": { "version": "1.19.0", "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.19.0.tgz", @@ -18418,14 +18647,37 @@ "url": "https://opencollective.com/parcel" } }, + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.19.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.19.0.tgz", + "integrity": "sha512-C+VuUTeSUOAaBZZOPT7Etn/agx/MatzJzGRkeV+zEABmPuntv1zihncsi+AyGmjkkzq3wVedEy7h0/4S84mUtg==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, "node_modules/lilconfig": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.0.0.tgz", - "integrity": "sha512-K2U4W2Ff5ibV7j7ydLr+zLAkIg5JJ4lPn1Ltsdt+Tz/IjQ8buJ55pZAxoP34lqIiwtF9iAvtLv3JGv7CAyAg+g==", + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.1.tgz", + "integrity": "sha512-O18pf7nyvHTckunPWCV1XUNXU1piu01y2b7ATJ0ppkUkk8ocqVWBrYjJBCwHDjD/ZWcfyrA0P4gKhzWGi5EINQ==", "dev": true, "license": "MIT", "engines": { "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antonk52" } }, "node_modules/lines-and-columns": { @@ -18435,22 +18687,22 @@ "license": "MIT" }, "node_modules/lint-staged": { - "version": "15.2.2", - "resolved": "https://registry.npmjs.org/lint-staged/-/lint-staged-15.2.2.tgz", - "integrity": "sha512-TiTt93OPh1OZOsb5B7k96A/ATl2AjIZo+vnzFZ6oHK5FuTk63ByDtxGQpHm+kFETjEWqgkF95M8FRXKR/LEBcw==", + "version": "15.2.4", + "resolved": "https://registry.npmjs.org/lint-staged/-/lint-staged-15.2.4.tgz", + "integrity": "sha512-3F9KRQIS2fVDGtCkBp4Bx0jswjX7zUcKx6OF0ZeY1prksUyKPRIIUqZhIUYAstJfvj6i48VFs4dwVIbCYwvTYQ==", "dev": true, "license": "MIT", "dependencies": { "chalk": "5.3.0", - "commander": "11.1.0", + "commander": "12.1.0", "debug": "4.3.4", "execa": "8.0.1", - "lilconfig": "3.0.0", - "listr2": "8.0.1", - "micromatch": "4.0.5", + "lilconfig": "3.1.1", + "listr2": "8.2.1", + "micromatch": "4.0.6", "pidtree": "0.6.0", "string-argv": "0.3.2", - "yaml": "2.3.4" + "yaml": "2.4.2" }, "bin": { "lint-staged": "bin/lint-staged.js" @@ -18476,19 +18728,46 @@ } }, "node_modules/lint-staged/node_modules/commander": { - "version": "11.1.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-11.1.0.tgz", - "integrity": "sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ==", + "version": "12.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-12.1.0.tgz", + "integrity": "sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==", "dev": true, "license": "MIT", "engines": { - "node": ">=16" + "node": ">=18" + } + }, + "node_modules/lint-staged/node_modules/micromatch": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.6.tgz", + "integrity": "sha512-Y4Ypn3oujJYxJcMacVgcs92wofTHxp9FzfDpQON4msDefoC0lb3ETvQLOdLcbhSwU1bz8HrL/1sygfBIHudrkQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^4.0.2" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/lint-staged/node_modules/picomatch": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz", + "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" } }, "node_modules/listr2": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/listr2/-/listr2-8.0.1.tgz", - "integrity": "sha512-ovJXBXkKGfq+CwmKTjluEqFi3p4h8xvkxGQQAQan22YCgef4KZ1mKGjzfGh6PL6AW5Csw0QiQPNuQyH+6Xk3hA==", + "version": "8.2.1", + "resolved": "https://registry.npmjs.org/listr2/-/listr2-8.2.1.tgz", + "integrity": "sha512-irTfvpib/rNiD637xeevjO2l3Z5loZmuaRi0L0YE5LfijwVY96oyVn0DFD3o/teAok7nfobMG1THvvcHh/BP6g==", "dev": true, "license": "MIT", "dependencies": { @@ -18496,7 +18775,7 @@ "colorette": "^2.0.20", "eventemitter3": "^5.0.1", "log-update": "^6.0.0", - "rfdc": "^1.3.0", + "rfdc": "^1.3.1", "wrap-ansi": "^9.0.0" }, "engines": { @@ -19741,12 +20020,12 @@ } }, "node_modules/micromatch": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz", - "integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==", + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.7.tgz", + "integrity": "sha512-LPP/3KorzCwBxfeUuZmaR6bG2kdeHSbe0P2tY3FLRU4vYrjYz5hI4QZwV0njUx3jeuKe67YukQ1LSPZBKDqO/Q==", "license": "MIT", "dependencies": { - "braces": "^3.0.2", + "braces": "^3.0.3", "picomatch": "^2.3.1" }, "engines": { @@ -20743,6 +21022,7 @@ "version": "0.1.5", "resolved": "https://registry.npmjs.org/osenv/-/osenv-0.1.5.tgz", "integrity": "sha512-0CWcCECdMVc2Rw3U5w9ZjqX6ga6ubk1xDVKxtBQPK7wis/0F2r9T6k4ydGYhecl7YUBxBVxhL5oisPsNxAPe2g==", + "deprecated": "This package is no longer supported.", "license": "ISC", "dependencies": { "os-homedir": "^1.0.0", @@ -21565,9 +21845,9 @@ } }, "node_modules/react-hook-form": { - "version": "7.51.4", - "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.51.4.tgz", - "integrity": "sha512-V14i8SEkh+V1gs6YtD0hdHYnoL4tp/HX/A45wWQN15CYr9bFRmmRdYStSO5L65lCCZRF+kYiSKhm9alqbcdiVA==", + "version": "7.51.5", + "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.51.5.tgz", + "integrity": "sha512-J2ILT5gWx1XUIJRETiA7M19iXHlG74+6O3KApzvqB/w8S5NQR7AbU8HVZrMALdmDgWpRPYiZJl0zx8Z4L2mP6Q==", "license": "MIT", "engines": { "node": ">=12.22.0" @@ -23910,14 +24190,14 @@ } }, "node_modules/supabase/node_modules/glob": { - "version": "10.3.15", - "resolved": "https://registry.npmjs.org/glob/-/glob-10.3.15.tgz", - "integrity": "sha512-0c6RlJt1TICLyvJYIApxb8GsXoai0KUP7AxKKAtsYXdgJR1mGEUa7DgwShbdk1nly0PYoZj01xd4hzbq3fsjpw==", + "version": "10.3.16", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.3.16.tgz", + "integrity": "sha512-JDKXl1DiuuHJ6fVS2FXjownaavciiHNUU4mOvV/B793RLh05vZL1rcPnCSaOgv1hDT6RDlY7AB7ZUvFYAtPgAw==", "dev": true, "license": "ISC", "dependencies": { "foreground-child": "^3.1.0", - "jackspeak": "^2.3.6", + "jackspeak": "^3.1.2", "minimatch": "^9.0.1", "minipass": "^7.0.4", "path-scurry": "^1.11.0" @@ -24798,9 +25078,9 @@ } }, "node_modules/undici": { - "version": "6.17.0", - "resolved": "https://registry.npmjs.org/undici/-/undici-6.17.0.tgz", - "integrity": "sha512-fs13QiDjPIzJ7gFAOal9CSG0c92rT2xw6MuMUJ4H30Eg5GCauLWYCCZA1tInjd6M4y+JZjVCCFr9pFpbhcC64w==", + "version": "6.18.1", + "resolved": "https://registry.npmjs.org/undici/-/undici-6.18.1.tgz", + "integrity": "sha512-/0BWqR8rJNRysS5lqVmfc7eeOErcOP4tZpATVjJOojjHZ71gSYVAtFhEmadcIjwMIUehh5NFyKGsXCnXIajtbA==", "license": "MIT", "engines": { "node": ">=18.17" @@ -25640,10 +25920,13 @@ "license": "ISC" }, "node_modules/yaml": { - "version": "2.3.4", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.3.4.tgz", - "integrity": "sha512-8aAvwVUSHpfEqTQ4w/KMlf3HcRdt50E5ODIQJBw1fQ5RL34xabzxtUlzTXVqc4rkZsPbvrXKWnABCD7kWSmocA==", + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.4.2.tgz", + "integrity": "sha512-B3VqDZ+JAg1nZpaEmWtTXUlBneoGx6CPM9b0TENK6aoSu5t73dItudwdgmi6tHlIZZId4dZ9skcAQ2UbcyAeVA==", "license": "ISC", + "bin": { + "yaml": "bin.mjs" + }, "engines": { "node": ">= 14" } diff --git a/package.json b/package.json index 575ff81..4a68399 100644 --- a/package.json +++ b/package.json @@ -24,7 +24,7 @@ "@fortawesome/fontawesome-svg-core": "6.5.2", "@fortawesome/free-solid-svg-icons": "6.5.2", "@fortawesome/react-native-fontawesome": "0.3.1", - "@hookform/resolvers": "3.4.0", + "@hookform/resolvers": "3.4.2", "@react-native-async-storage/async-storage": "1.23.1", "@react-navigation/native": "6.1.17", "@supabase/supabase-js": "2.43.2", @@ -39,7 +39,7 @@ "lottie-react-native": "6.7.0", "react": "18.2.0", "react-dom": "18.2.0", - "react-hook-form": "7.51.4", + "react-hook-form": "7.51.5", "react-native": "0.74.1", "react-native-calendars": "1.1305.0", "react-native-circular-progress-indicator": "4.4.2", @@ -68,21 +68,21 @@ "@types/node": "20.12.12", "@types/react": "18.2.79", "@types/react-test-renderer": "18.3.0", - "@typescript-eslint/eslint-plugin": "7.9.0", - "@typescript-eslint/parser": "7.9.0", + "@typescript-eslint/eslint-plugin": "7.10.0", + "@typescript-eslint/parser": "7.10.0", "eslint": "8.57.0", - "eslint-config-conventions": "14.1.0", + "eslint-config-conventions": "14.2.0", "eslint-plugin-import": "2.29.1", "eslint-plugin-promise": "6.1.1", "eslint-plugin-react": "7.34.1", "eslint-plugin-react-hooks": "4.6.2", "eslint-plugin-react-native": "4.1.0", - "eslint-plugin-unicorn": "51.0.1", + "eslint-plugin-unicorn": "53.0.0", "husky": "9.0.11", "jest": "29.7.0", "jest-expo": "51.0.2", "jest-junit": "16.0.0", - "lint-staged": "15.2.2", + "lint-staged": "15.2.4", "prettier": "3.2.5", "react-test-renderer": "18.2.0", "supabase": "1.167.4", diff --git a/presentation/react-native/components/HabitsMainPage/HabitsList.tsx b/presentation/react-native/components/HabitsMainPage/HabitsList.tsx index 28abb12..28c0671 100644 --- a/presentation/react-native/components/HabitsMainPage/HabitsList.tsx +++ b/presentation/react-native/components/HabitsMainPage/HabitsList.tsx @@ -6,7 +6,7 @@ import { Divider, List, Text } from "react-native-paper" import { GOAL_FREQUENCIES, type GoalFrequency } from "@/domain/entities/Goal" import type { HabitHistory } from "@/domain/entities/HabitHistory" import type { HabitsTracker } from "@/domain/entities/HabitsTracker" -import { capitalize } from "@/utils/strings" +import { LOCALE, capitalize } from "@/utils/strings" import confettiJSON from "../../../assets/confetti.json" import { HabitCard } from "./HabitCard" @@ -96,7 +96,7 @@ export const HabitsList: React.FC = (props) => { marginTop: 20, }} > - {selectedDate.toLocaleDateString("en-US", { + {selectedDate.toLocaleDateString(LOCALE, { weekday: "long", year: "numeric", month: "long", diff --git a/presentation/react-native/components/HabitsStatistics.tsx b/presentation/react-native/components/HabitsStatistics.tsx new file mode 100644 index 0000000..46ac600 --- /dev/null +++ b/presentation/react-native/components/HabitsStatistics.tsx @@ -0,0 +1,110 @@ +import { Card, Divider, Text } from "react-native-paper" +import CircularProgress from "react-native-circular-progress-indicator" +import { Agenda } from "react-native-calendars" +import { useState } from "react" + +import { getNowDateUTC, getISODate } from "@/utils/dates" +import type { HabitsTracker } from "@/domain/entities/HabitsTracker" +import { LOCALE } from "@/utils/strings" +import { + GOAL_FREQUENCIES, + GOAL_FREQUENCIES_TYPES, +} from "@/domain/entities/Goal" +import { calculateRatio } from "@/utils/maths" + +export interface HabitsStatisticsProps { + habitsTracker: HabitsTracker +} + +export const HabitsStatistics: React.FC = (props) => { + const { habitsTracker } = props + + const today = getNowDateUTC() + const todayISO = getISODate(today) + + const [selectedDate, setSelectedDate] = useState(today) + const selectedDateISO = getISODate(selectedDate) + + return ( + { + setSelectedDate(new Date(date.dateString)) + }} + markedDates={{ + [todayISO]: { marked: true, today: true }, + }} + maxDate={todayISO} + selected={selectedDateISO} + renderList={() => { + return ( + <> + + + + {selectedDate.toLocaleDateString(LOCALE, { + weekday: "long", + year: "numeric", + month: "long", + day: "numeric", + })} + + + {GOAL_FREQUENCIES.map((frequency) => { + const { totalGoalsSuccess, totalGoals } = + habitsTracker.getHabitsStatisticsByDateAndFrequency({ + selectedDate, + frequency, + }) + const percentage = + calculateRatio(totalGoalsSuccess, totalGoals) * 100 + return { + totalGoalsSuccess, + totalGoals, + percentage, + frequency, + } + }) + .filter(({ totalGoals }) => { + return totalGoals > 0 + }) + .map( + ({ frequency, totalGoals, totalGoalsSuccess, percentage }) => { + return ( + + + + {totalGoalsSuccess} achieved goals in the{" "} + {GOAL_FREQUENCIES_TYPES[frequency]} out of{" "} + {totalGoals}. + + + + + ) + }, + )} + + ) + }} + /> + ) +} diff --git a/presentation/react/components/Stats/Stats.tsx b/presentation/react/components/Stats/Stats.tsx deleted file mode 100644 index bf3d9f8..0000000 --- a/presentation/react/components/Stats/Stats.tsx +++ /dev/null @@ -1,167 +0,0 @@ -import { Card, Text } from "react-native-paper" -import CircularProgress from "react-native-circular-progress-indicator" -import { Agenda } from "react-native-calendars" -import { useState } from "react" - -import { getNowDateUTC, getISODate } from "@/utils/dates" -import type { HabitsTracker } from "@/domain/entities/HabitsTracker" - -export interface StatsProps { - habitsTracker: HabitsTracker -} - -export const Stats: React.FC = (props) => { - const { habitsTracker } = props - - const today = getNowDateUTC() - const todayISO = getISODate(today) - - const [selectedDate, setSelectedDate] = useState(today) - const selectedDateISO = getISODate(selectedDate) - - const habitsHistory = habitsTracker.getAllHabitsHistory() - - let goalDays = 0 - let totalGoalDays = 0 - const dailyHabits = habitsHistory.filter((el) => { - return ( - el.habit.goal.frequency === "daily" && el.habit.startDate <= selectedDate - ) - }) - let displayDaily = true - if (dailyHabits.length === 0) { - displayDaily = false - } else { - for (const el of dailyHabits) { - totalGoalDays++ - if ( - el.getProgressesByDate(selectedDate)[0]?.goalProgress.isCompleted() ?? - false - ) { - goalDays++ - } - } - } - - let goalWeek = 0 - let totalGoalWeek = 0 - const weeklyHabits = habitsHistory.filter((el) => { - return ( - el.habit.goal.frequency === "weekly" && el.habit.startDate <= selectedDate - ) - }) - - let displayWeekly = true - if (weeklyHabits.length === 0) { - displayWeekly = false - } else { - for (const el of weeklyHabits) { - totalGoalWeek++ - if ( - el.getProgressesByDate(selectedDate)[0]?.goalProgress.isCompleted() ?? - false - ) { - goalWeek++ - } - } - } - - let goalMonth = 0 - let totalGoalMonth = 0 - const monthlyHabits = habitsHistory.filter((el) => { - return ( - el.habit.goal.frequency === "monthly" && - el.habit.startDate <= selectedDate - ) - }) - - let displayMonthly = true - if (monthlyHabits.length === 0) { - displayMonthly = false - } else { - for (const el of monthlyHabits) { - totalGoalMonth++ - if ( - el.getProgressesByDate(selectedDate)[0]?.goalProgress.isCompleted() ?? - false - ) { - goalMonth++ - } - } - } - - return ( - { - setSelectedDate(new Date(date.dateString)) - }} - markedDates={{ - [todayISO]: { marked: true, today: true }, - }} - maxDate={todayISO} - selected={selectedDateISO} - renderList={() => { - return ( - <> - {displayDaily ? ( - - - - - {goalDays} but réussi dans la journée sur {totalGoalDays} - - - - - ) : null} - {displayWeekly ? ( - - - - - {goalWeek} but réussi dans la semaine sur {totalGoalWeek} - - - - - ) : null} - {displayMonthly ? ( - - - - - {goalMonth} but réussi dans le mois sur {totalGoalMonth} - - - - - ) : null} - - ) - }} - /> - ) -} diff --git a/utils/__tests__/maths.test.ts b/utils/__tests__/maths.test.ts new file mode 100644 index 0000000..39cbcaa --- /dev/null +++ b/utils/__tests__/maths.test.ts @@ -0,0 +1,83 @@ +import { calculateRatio } from "../maths" + +describe("utils/maths", () => { + describe("calculateRatio", () => { + it("should calculate the ratio of a value to a total", () => { + // Arrange - Given + const value = 3 + const total = 10 + + // Act - When + const result = calculateRatio(value, total) + + // Assert - Then + const expected = 0.3 + expect(result).toEqual(expected) + }) + + it("should return 0 if the total is 0", () => { + // Arrange - Given + const value = 3 + const total = 0 + + // Act - When + const result = calculateRatio(value, total) + + // Assert - Then + const expected = 0 + expect(result).toEqual(expected) + }) + + it("should return 0 if the total is negative", () => { + // Arrange - Given + const value = 3 + const total = -1 + + // Act - When + const result = calculateRatio(value, total) + + // Assert - Then + const expected = 0 + expect(result).toEqual(expected) + }) + + it("should return 0 if the value is 0", () => { + // Arrange - Given + const value = 0 + const total = 10 + + // Act - When + const result = calculateRatio(value, total) + + // Assert - Then + const expected = 0 + expect(result).toEqual(expected) + }) + + it("should return 1 if the value is equal to the total", () => { + // Arrange - Given + const value = 10 + const total = 10 + + // Act - When + const result = calculateRatio(value, total) + + // Assert - Then + const expected = 1 + expect(result).toEqual(expected) + }) + + it("should return 1 if the value is greater than the total", () => { + // Arrange - Given + const value = 11 + const total = 10 + + // Act - When + const result = calculateRatio(value, total) + + // Assert - Then + const expected = 1 + expect(result).toEqual(expected) + }) + }) +}) diff --git a/utils/maths.ts b/utils/maths.ts new file mode 100644 index 0000000..003966d --- /dev/null +++ b/utils/maths.ts @@ -0,0 +1,9 @@ +export const calculateRatio = (value: number, total: number): number => { + if (total <= 0) { + return 0 + } + if (value >= total) { + return 1 + } + return value / total +} diff --git a/utils/strings.ts b/utils/strings.ts index df75057..2f4dc53 100644 --- a/utils/strings.ts +++ b/utils/strings.ts @@ -1,3 +1,5 @@ +export const LOCALE = "en-US" + export const capitalize = (string: string): string => { return string.charAt(0).toUpperCase() + string.slice(1) }