diff --git a/app/application/habits/[habitId]/index.tsx b/app/application/habits/[habitId]/index.tsx
index 6236764..a7fcdec 100644
--- a/app/application/habits/[habitId]/index.tsx
+++ b/app/application/habits/[habitId]/index.tsx
@@ -12,6 +12,7 @@ const HabitPage: React.FC = () => {
if (habitHistory == null) {
return
}
+
return (
{
- const [selected, setSelected] = useState("")
+ const today = useMemo(() => {
+ return new Date()
+ }, [])
+ const todayISO = getISODate(today)
+
+ const [selectedDate, setSelectedDate] = useState(today)
+ const selectedISODate = getISODate(selectedDate)
return (
- {
- setSelected(day.dateString)
+ {
+ setSelectedDate(new Date(date.dateString))
}}
markedDates={{
- "2023-03-01": { selected: true, marked: true, selectedColor: "blue" },
- "2023-03-02": { marked: true },
- "2023-03-03": { selected: true, marked: true, selectedColor: "blue" },
- [selected]: {
- selected: true,
- disableTouchEvent: true,
- selectedColor: "orange",
- },
+ [todayISO]: { marked: true },
}}
- theme={{
- backgroundColor: "#000000",
- calendarBackground: "#000000",
- textSectionTitleColor: "#b6c1cd",
- selectedDayBackgroundColor: "#00adf5",
- selectedDayTextColor: "#ffffff",
- todayTextColor: "#00adf5",
- dayTextColor: "#2d4150",
- textDisabledColor: "#d9efff",
+ selected={selectedISODate}
+ renderList={() => {
+ return (
+
+ {selectedDate.toISOString()}
+
+ )
}}
/>
diff --git a/app/application/habits/index.tsx b/app/application/habits/index.tsx
index adcc933..9857a24 100644
--- a/app/application/habits/index.tsx
+++ b/app/application/habits/index.tsx
@@ -16,6 +16,7 @@ const HabitsPage: React.FC = () => {
style={[
{
flex: 1,
+ backgroundColor: "white",
alignItems: "center",
justifyContent:
retrieveHabitsTracker.state === "loading" ||
diff --git a/domain/entities/Goal.ts b/domain/entities/Goal.ts
index 1f1cad1..ded0b30 100644
--- a/domain/entities/Goal.ts
+++ b/domain/entities/Goal.ts
@@ -86,6 +86,24 @@ export abstract class GoalProgress implements GoalProgressBase {
public abstract isCompleted(): boolean
public abstract toJSON(): GoalProgressBase
+
+ public static isNumeric(
+ goalProgress: GoalProgress,
+ ): goalProgress is GoalNumericProgress {
+ return goalProgress.goal.isNumeric()
+ }
+ public isNumeric(): this is GoalNumericProgress {
+ return GoalProgress.isNumeric(this)
+ }
+
+ public static isBoolean(
+ goalProgress: GoalProgress,
+ ): goalProgress is GoalBooleanProgress {
+ return goalProgress.goal.isBoolean()
+ }
+ public isBoolean(): this is GoalBooleanProgress {
+ return GoalProgress.isBoolean(this)
+ }
}
interface GoalNumericOptions extends GoalBase {
diff --git a/domain/entities/HabitHistory.ts b/domain/entities/HabitHistory.ts
index 8241b5c..b7f7a08 100644
--- a/domain/entities/HabitHistory.ts
+++ b/domain/entities/HabitHistory.ts
@@ -1,5 +1,8 @@
+import { getISODate, getWeekNumber } from "@/utils/dates"
import type { Habit } from "./Habit"
import type { HabitProgress } from "./HabitProgress"
+import type { GoalProgress } from "./Goal"
+import { GoalBooleanProgress, GoalNumericProgress } from "./Goal"
export interface HabitHistoryJSON {
habit: Habit
@@ -8,11 +11,70 @@ export interface HabitHistoryJSON {
export class HabitHistory implements HabitHistoryJSON {
public habit: Habit
- public progressHistory: HabitProgress[]
+
+ private _progressHistory: HabitProgress[] = []
public constructor(options: HabitHistoryJSON) {
const { habit, progressHistory } = options
this.habit = habit
this.progressHistory = progressHistory
}
+
+ /**
+ * Progress History sorted chronologically (from old to most recent progress at the end).
+ */
+ public get progressHistory(): HabitProgress[] {
+ return this._progressHistory
+ }
+
+ public set progressHistory(progressHistory: HabitProgress[]) {
+ this._progressHistory = [...progressHistory]
+ this._progressHistory.sort((a, b) => {
+ return a.date.getTime() - b.date.getTime()
+ })
+ }
+
+ private getProgressesByDate(date: Date): HabitProgress[] {
+ return this._progressHistory.filter((progress) => {
+ if (this.habit.goal.frequency === "monthly") {
+ return (
+ date.getFullYear() === progress.date.getFullYear() &&
+ date.getMonth() === progress.date.getMonth()
+ )
+ }
+ if (this.habit.goal.frequency === "weekly") {
+ return (
+ getWeekNumber(date) === getWeekNumber(progress.date) &&
+ date.getFullYear() === progress.date.getFullYear()
+ )
+ }
+ if (this.habit.goal.frequency === "daily") {
+ return getISODate(date) === getISODate(progress.date)
+ }
+ return false
+ })
+ }
+
+ public getGoalProgressByDate(date: Date): GoalProgress {
+ const progresses = this.getProgressesByDate(date)
+ if (this.habit.goal.isBoolean()) {
+ const lastSavedProgress = progresses[progresses.length - 1]
+ return new GoalBooleanProgress({
+ goal: this.habit.goal,
+ progress: lastSavedProgress?.goalProgress?.isCompleted() ?? false,
+ })
+ }
+
+ if (this.habit.goal.isNumeric()) {
+ return new GoalNumericProgress({
+ goal: this.habit.goal,
+ progress: progresses.reduce((sum, current) => {
+ const goalProgress = current.goalProgress as GoalNumericProgress
+ return sum + goalProgress.progress
+ }, 0),
+ })
+ }
+
+ throw new Error("Invalid")
+ }
}
diff --git a/domain/use-cases/RetrieveHabitsTracker.ts b/domain/use-cases/RetrieveHabitsTracker.ts
index dd04f40..58b4151 100644
--- a/domain/use-cases/RetrieveHabitsTracker.ts
+++ b/domain/use-cases/RetrieveHabitsTracker.ts
@@ -39,9 +39,7 @@ export class RetrieveHabitsTrackerUseCase
})
return new HabitHistory({
habit,
- progressHistory: progressHistory.sort((a, b) => {
- return a.date.getTime() - b.date.getTime()
- }),
+ progressHistory,
})
}),
)
diff --git a/presentation/presenters/Authentication.ts b/presentation/presenters/Authentication.ts
index 97acf31..5632043 100644
--- a/presentation/presenters/Authentication.ts
+++ b/presentation/presenters/Authentication.ts
@@ -8,7 +8,7 @@ import type {
import type { AuthenticationUseCase } from "@/domain/use-cases/Authentication"
import type { ErrorGlobal, FetchState } from "./_Presenter"
import { Presenter } from "./_Presenter"
-import { getErrorsFieldsFromZodError } from "./utils/zod"
+import { getErrorsFieldsFromZodError } from "../../utils/zod"
export interface AuthenticationPresenterState {
user: User | null
diff --git a/presentation/presenters/HabitsTracker.ts b/presentation/presenters/HabitsTracker.ts
index f1dc56a..b619559 100644
--- a/presentation/presenters/HabitsTracker.ts
+++ b/presentation/presenters/HabitsTracker.ts
@@ -8,7 +8,7 @@ import type {
RetrieveHabitsTrackerUseCaseOptions,
} from "@/domain/use-cases/RetrieveHabitsTracker"
import type { HabitCreateData } from "@/domain/entities/Habit"
-import { getErrorsFieldsFromZodError } from "./utils/zod"
+import { getErrorsFieldsFromZodError } from "../../utils/zod"
import type { HabitCreateUseCase } from "@/domain/use-cases/HabitCreate"
export interface HabitsTrackerPresenterState {
diff --git a/presentation/react/components/HabitsMainPage/HabitCard.tsx b/presentation/react/components/HabitsMainPage/HabitCard.tsx
index 6526d38..3cf9979 100644
--- a/presentation/react/components/HabitsMainPage/HabitCard.tsx
+++ b/presentation/react/components/HabitsMainPage/HabitCard.tsx
@@ -1,17 +1,19 @@
import FontAwesome6 from "@expo/vector-icons/FontAwesome6"
import { useRouter } from "expo-router"
-import { List } from "react-native-paper"
+import { View } from "react-native"
+import { List, Text } from "react-native-paper"
-import type { HabitHistory as HabitHistoryType } from "@/domain/entities/HabitHistory"
-import { getColorRGBAFromHex } from "@/presentation/presenters/utils/colors"
+import type { GoalProgress } from "@/domain/entities/Goal"
+import type { Habit } from "@/domain/entities/Habit"
+import { getColorRGBAFromHex } from "@/utils/colors"
export interface HabitCardProps {
- habitHistory: HabitHistoryType
+ habit: Habit
+ goalProgress: GoalProgress
}
export const HabitCard: React.FC = (props) => {
- const { habitHistory } = props
- const { habit } = habitHistory
+ const { habit, goalProgress } = props
const router = useRouter()
@@ -63,6 +65,25 @@ export const HabitCard: React.FC = (props) => {
/>
)
}}
+ right={() => {
+ if (goalProgress.isNumeric()) {
+ return (
+
+
+ {goalProgress.progress.toLocaleString()} /{" "}
+ {goalProgress.goal.target.value.toLocaleString()}{" "}
+ {goalProgress.goal.target.unit}
+
+
+ )
+ }
+
+ return (
+
+ {goalProgress.isCompleted() ? "true" : "false"}
+
+ )
+ }}
/>
)
}
diff --git a/presentation/react/components/HabitsMainPage/HabitsEmpty.tsx b/presentation/react/components/HabitsMainPage/HabitsEmpty.tsx
new file mode 100644
index 0000000..3c5b33f
--- /dev/null
+++ b/presentation/react/components/HabitsMainPage/HabitsEmpty.tsx
@@ -0,0 +1,40 @@
+import { useRouter } from "expo-router"
+import { View } from "react-native"
+import { Button, Text } from "react-native-paper"
+
+export const HabitsEmpty: React.FC = () => {
+ const router = useRouter()
+
+ return (
+
+ {"Let's begin by adding habits! 🤩"}
+
+
+ )
+}
diff --git a/presentation/react/components/HabitsMainPage/HabitsList.tsx b/presentation/react/components/HabitsMainPage/HabitsList.tsx
new file mode 100644
index 0000000..abd4109
--- /dev/null
+++ b/presentation/react/components/HabitsMainPage/HabitsList.tsx
@@ -0,0 +1,81 @@
+import { useState } from "react"
+import { Dimensions, ScrollView } from "react-native"
+import { List } from "react-native-paper"
+
+import type { GoalFrequency } from "@/domain/entities/Goal"
+import { GOAL_FREQUENCIES } from "@/domain/entities/Goal"
+import type { HabitsTracker } from "@/domain/entities/HabitsTracker"
+import { capitalize } from "@/utils/strings"
+import { HabitCard } from "./HabitCard"
+import { HabitsEmpty } from "./HabitsEmpty"
+
+export interface HabitsListProps {
+ habitsTracker: HabitsTracker
+ selectedDate: Date
+}
+
+export const HabitsList: React.FC = (props) => {
+ const { habitsTracker, selectedDate } = props
+
+ const frequenciesFiltered = GOAL_FREQUENCIES.filter((frequency) => {
+ return habitsTracker.habitsHistory[frequency].length > 0
+ })
+
+ const [accordionExpanded, setAccordionExpanded] = useState<{
+ [key in GoalFrequency]: boolean
+ }>({
+ daily: true,
+ weekly: true,
+ monthly: true,
+ })
+
+ if (frequenciesFiltered.length <= 0) {
+ return
+ }
+
+ return (
+
+
+ {frequenciesFiltered.map((frequency) => {
+ return (
+ {
+ setAccordionExpanded((old) => {
+ return {
+ ...old,
+ [frequency]: !old[frequency],
+ }
+ })
+ }}
+ key={frequency}
+ title={capitalize(frequency)}
+ titleStyle={[
+ {
+ fontSize: 26,
+ },
+ ]}
+ >
+ {habitsTracker.habitsHistory[frequency].map((item) => {
+ const goalProgress = item.getGoalProgressByDate(selectedDate)
+ return (
+
+ )
+ })}
+
+ )
+ })}
+
+
+ )
+}
diff --git a/presentation/react/components/HabitsMainPage/HabitsMainPage.tsx b/presentation/react/components/HabitsMainPage/HabitsMainPage.tsx
index 540df7f..ebe5492 100644
--- a/presentation/react/components/HabitsMainPage/HabitsMainPage.tsx
+++ b/presentation/react/components/HabitsMainPage/HabitsMainPage.tsx
@@ -1,13 +1,9 @@
-import { useRouter } from "expo-router"
-import { useState } from "react"
-import { Dimensions, ScrollView, View } from "react-native"
-import { Button, List, Text } from "react-native-paper"
+import { useMemo, useState } from "react"
+import { Agenda } from "react-native-calendars"
-import type { GoalFrequency } from "@/domain/entities/Goal"
-import { GOAL_FREQUENCIES } from "@/domain/entities/Goal"
import type { HabitsTracker } from "@/domain/entities/HabitsTracker"
-import { capitalize } from "@/presentation/presenters/utils/strings"
-import { HabitCard } from "./HabitCard"
+import { getISODate } from "@/utils/dates"
+import { HabitsList } from "./HabitsList"
export interface HabitsMainPageProps {
habitsTracker: HabitsTracker
@@ -16,91 +12,34 @@ export interface HabitsMainPageProps {
export const HabitsMainPage: React.FC = (props) => {
const { habitsTracker } = props
- const router = useRouter()
+ const today = useMemo(() => {
+ return new Date()
+ }, [])
+ const todayISO = getISODate(today)
- const habitsByFrequency = GOAL_FREQUENCIES.filter((frequency) => {
- return habitsTracker.habitsHistory[frequency].length > 0
- })
-
- const [accordionExpanded, setAccordionExpanded] = useState<{
- [key in GoalFrequency]: boolean
- }>({
- daily: true,
- weekly: true,
- monthly: true,
- })
-
- if (habitsByFrequency.length <= 0) {
- return (
-
- {"Let's begin by adding habits! 🤩"}
-
-
- )
- }
+ const [selectedDate, setSelectedDate] = useState(today)
+ const selectedISODate = getISODate(selectedDate)
return (
- {
+ setSelectedDate(new Date(date.dateString))
}}
- >
-
- {habitsByFrequency.map((frequency) => {
- return (
- {
- setAccordionExpanded((old) => {
- return {
- ...old,
- [frequency]: !old[frequency],
- }
- })
- }}
- key={frequency}
- title={capitalize(frequency)}
- titleStyle={[
- {
- fontSize: 26,
- },
- ]}
- >
- {habitsTracker.habitsHistory[frequency].map((item) => {
- return
- })}
-
- )
- })}
-
-
+ markedDates={{
+ [todayISO]: { marked: true },
+ }}
+ selected={selectedISODate}
+ renderList={() => {
+ return (
+
+ )
+ }}
+ />
)
}
diff --git a/presentation/react/hooks/usePresenterState.ts b/presentation/react/hooks/usePresenterState.ts
index 55717ce..6828ccd 100644
--- a/presentation/react/hooks/usePresenterState.ts
+++ b/presentation/react/hooks/usePresenterState.ts
@@ -2,11 +2,13 @@ import { useEffect, useState } from "react"
import type { Presenter } from "@/presentation/presenters/_Presenter"
-export const usePresenterState = (presenter: Presenter): S => {
- const [state, setState] = useState(presenter.initialState)
+export const usePresenterState = (
+ presenter: Presenter,
+): State => {
+ const [state, setState] = useState(presenter.initialState)
useEffect(() => {
- const presenterSubscription = (state: S): void => {
+ const presenterSubscription = (state: State): void => {
setState(state)
}
diff --git a/presentation/presenters/utils/colors.ts b/utils/colors.ts
similarity index 100%
rename from presentation/presenters/utils/colors.ts
rename to utils/colors.ts
diff --git a/utils/dates.ts b/utils/dates.ts
new file mode 100644
index 0000000..43da5a0
--- /dev/null
+++ b/utils/dates.ts
@@ -0,0 +1,35 @@
+export const ONE_DAY_MILLISECONDS = 1_000 * 60 * 60 * 24
+
+/**
+ * Returns a date as a string value in ISO format (without time information).
+ *
+ * @param date
+ * @returns
+ * @example getISODate(new Date("2012-05-23")) // "2012-05-23"
+ */
+export const getISODate = (date: Date): string => {
+ return date.toISOString().slice(0, 10)
+}
+
+/**
+ * Get the week number [1-52] for a given date.
+ * @param {Date} date
+ * @returns {number}
+ * @example getWeekNumber(new Date(2020, 0, 1)) // 1
+ * @example getWeekNumber(new Date(2020, 0, 8)) // 2
+ */
+export const getWeekNumber = (date: Date): number => {
+ const dateCopy = new Date(date.getTime())
+ dateCopy.setHours(0, 0, 0, 0)
+ dateCopy.setDate(dateCopy.getDate() + 3 - ((dateCopy.getDay() + 6) % 7))
+ const week1 = new Date(dateCopy.getFullYear(), 0, 4)
+ return (
+ 1 +
+ Math.round(
+ ((dateCopy.getTime() - week1.getTime()) / ONE_DAY_MILLISECONDS -
+ 3 +
+ ((week1.getDay() + 6) % 7)) /
+ 7,
+ )
+ )
+}
diff --git a/presentation/presenters/utils/strings.ts b/utils/strings.ts
similarity index 100%
rename from presentation/presenters/utils/strings.ts
rename to utils/strings.ts
diff --git a/presentation/presenters/utils/zod.ts b/utils/zod.ts
similarity index 100%
rename from presentation/presenters/utils/zod.ts
rename to utils/zod.ts