feat: get habit goal progress for a given date
This commit is contained in:
parent
2ab83dfc89
commit
20b4456245
@ -12,6 +12,7 @@ const HabitPage: React.FC = () => {
|
|||||||
if (habitHistory == null) {
|
if (habitHistory == null) {
|
||||||
return <Redirect href="/application/habits/" />
|
return <Redirect href="/application/habits/" />
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SafeAreaView
|
<SafeAreaView
|
||||||
style={[
|
style={[
|
||||||
|
@ -1,43 +1,46 @@
|
|||||||
import { useState } from "react"
|
import { useMemo, useState } from "react"
|
||||||
import { Calendar } from "react-native-calendars"
|
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 { SafeAreaView } from "react-native-safe-area-context"
|
||||||
|
|
||||||
|
import { getISODate } from "@/utils/dates"
|
||||||
|
|
||||||
const HistoryPage: React.FC = () => {
|
const HistoryPage: React.FC = () => {
|
||||||
const [selected, setSelected] = useState("")
|
const today = useMemo(() => {
|
||||||
|
return new Date()
|
||||||
|
}, [])
|
||||||
|
const todayISO = getISODate(today)
|
||||||
|
|
||||||
|
const [selectedDate, setSelectedDate] = useState<Date>(today)
|
||||||
|
const selectedISODate = getISODate(selectedDate)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SafeAreaView
|
<SafeAreaView
|
||||||
style={[
|
style={[
|
||||||
{
|
{
|
||||||
flex: 1,
|
flex: 1,
|
||||||
alignItems: "center",
|
backgroundColor: "white",
|
||||||
justifyContent: "center",
|
|
||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
>
|
>
|
||||||
<Calendar
|
<Agenda
|
||||||
onDayPress={(day) => {
|
firstDay={1}
|
||||||
setSelected(day.dateString)
|
showClosingKnob
|
||||||
|
showOnlySelectedDayItems
|
||||||
|
onDayPress={(date) => {
|
||||||
|
setSelectedDate(new Date(date.dateString))
|
||||||
}}
|
}}
|
||||||
markedDates={{
|
markedDates={{
|
||||||
"2023-03-01": { selected: true, marked: true, selectedColor: "blue" },
|
[todayISO]: { marked: true },
|
||||||
"2023-03-02": { marked: true },
|
|
||||||
"2023-03-03": { selected: true, marked: true, selectedColor: "blue" },
|
|
||||||
[selected]: {
|
|
||||||
selected: true,
|
|
||||||
disableTouchEvent: true,
|
|
||||||
selectedColor: "orange",
|
|
||||||
},
|
|
||||||
}}
|
}}
|
||||||
theme={{
|
selected={selectedISODate}
|
||||||
backgroundColor: "#000000",
|
renderList={() => {
|
||||||
calendarBackground: "#000000",
|
return (
|
||||||
textSectionTitleColor: "#b6c1cd",
|
<View>
|
||||||
selectedDayBackgroundColor: "#00adf5",
|
<Text>{selectedDate.toISOString()}</Text>
|
||||||
selectedDayTextColor: "#ffffff",
|
</View>
|
||||||
todayTextColor: "#00adf5",
|
)
|
||||||
dayTextColor: "#2d4150",
|
|
||||||
textDisabledColor: "#d9efff",
|
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</SafeAreaView>
|
</SafeAreaView>
|
||||||
|
@ -16,6 +16,7 @@ const HabitsPage: React.FC = () => {
|
|||||||
style={[
|
style={[
|
||||||
{
|
{
|
||||||
flex: 1,
|
flex: 1,
|
||||||
|
backgroundColor: "white",
|
||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
justifyContent:
|
justifyContent:
|
||||||
retrieveHabitsTracker.state === "loading" ||
|
retrieveHabitsTracker.state === "loading" ||
|
||||||
|
@ -86,6 +86,24 @@ export abstract class GoalProgress implements GoalProgressBase {
|
|||||||
public abstract isCompleted(): boolean
|
public abstract isCompleted(): boolean
|
||||||
|
|
||||||
public abstract toJSON(): GoalProgressBase
|
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 {
|
interface GoalNumericOptions extends GoalBase {
|
||||||
|
@ -1,5 +1,8 @@
|
|||||||
|
import { getISODate, getWeekNumber } from "@/utils/dates"
|
||||||
import type { Habit } from "./Habit"
|
import type { Habit } from "./Habit"
|
||||||
import type { HabitProgress } from "./HabitProgress"
|
import type { HabitProgress } from "./HabitProgress"
|
||||||
|
import type { GoalProgress } from "./Goal"
|
||||||
|
import { GoalBooleanProgress, GoalNumericProgress } from "./Goal"
|
||||||
|
|
||||||
export interface HabitHistoryJSON {
|
export interface HabitHistoryJSON {
|
||||||
habit: Habit
|
habit: Habit
|
||||||
@ -8,11 +11,70 @@ export interface HabitHistoryJSON {
|
|||||||
|
|
||||||
export class HabitHistory implements HabitHistoryJSON {
|
export class HabitHistory implements HabitHistoryJSON {
|
||||||
public habit: Habit
|
public habit: Habit
|
||||||
public progressHistory: HabitProgress[]
|
|
||||||
|
private _progressHistory: HabitProgress[] = []
|
||||||
|
|
||||||
public constructor(options: HabitHistoryJSON) {
|
public constructor(options: HabitHistoryJSON) {
|
||||||
const { habit, progressHistory } = options
|
const { habit, progressHistory } = options
|
||||||
this.habit = habit
|
this.habit = habit
|
||||||
this.progressHistory = progressHistory
|
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")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -39,9 +39,7 @@ export class RetrieveHabitsTrackerUseCase
|
|||||||
})
|
})
|
||||||
return new HabitHistory({
|
return new HabitHistory({
|
||||||
habit,
|
habit,
|
||||||
progressHistory: progressHistory.sort((a, b) => {
|
progressHistory,
|
||||||
return a.date.getTime() - b.date.getTime()
|
|
||||||
}),
|
|
||||||
})
|
})
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
@ -8,7 +8,7 @@ import type {
|
|||||||
import type { AuthenticationUseCase } from "@/domain/use-cases/Authentication"
|
import type { AuthenticationUseCase } from "@/domain/use-cases/Authentication"
|
||||||
import type { ErrorGlobal, FetchState } from "./_Presenter"
|
import type { ErrorGlobal, FetchState } from "./_Presenter"
|
||||||
import { Presenter } from "./_Presenter"
|
import { Presenter } from "./_Presenter"
|
||||||
import { getErrorsFieldsFromZodError } from "./utils/zod"
|
import { getErrorsFieldsFromZodError } from "../../utils/zod"
|
||||||
|
|
||||||
export interface AuthenticationPresenterState {
|
export interface AuthenticationPresenterState {
|
||||||
user: User | null
|
user: User | null
|
||||||
|
@ -8,7 +8,7 @@ import type {
|
|||||||
RetrieveHabitsTrackerUseCaseOptions,
|
RetrieveHabitsTrackerUseCaseOptions,
|
||||||
} from "@/domain/use-cases/RetrieveHabitsTracker"
|
} from "@/domain/use-cases/RetrieveHabitsTracker"
|
||||||
import type { HabitCreateData } from "@/domain/entities/Habit"
|
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"
|
import type { HabitCreateUseCase } from "@/domain/use-cases/HabitCreate"
|
||||||
|
|
||||||
export interface HabitsTrackerPresenterState {
|
export interface HabitsTrackerPresenterState {
|
||||||
|
@ -1,17 +1,19 @@
|
|||||||
import FontAwesome6 from "@expo/vector-icons/FontAwesome6"
|
import FontAwesome6 from "@expo/vector-icons/FontAwesome6"
|
||||||
import { useRouter } from "expo-router"
|
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 type { GoalProgress } from "@/domain/entities/Goal"
|
||||||
import { getColorRGBAFromHex } from "@/presentation/presenters/utils/colors"
|
import type { Habit } from "@/domain/entities/Habit"
|
||||||
|
import { getColorRGBAFromHex } from "@/utils/colors"
|
||||||
|
|
||||||
export interface HabitCardProps {
|
export interface HabitCardProps {
|
||||||
habitHistory: HabitHistoryType
|
habit: Habit
|
||||||
|
goalProgress: GoalProgress
|
||||||
}
|
}
|
||||||
|
|
||||||
export const HabitCard: React.FC<HabitCardProps> = (props) => {
|
export const HabitCard: React.FC<HabitCardProps> = (props) => {
|
||||||
const { habitHistory } = props
|
const { habit, goalProgress } = props
|
||||||
const { habit } = habitHistory
|
|
||||||
|
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
|
|
||||||
@ -63,6 +65,25 @@ export const HabitCard: React.FC<HabitCardProps> = (props) => {
|
|||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
}}
|
}}
|
||||||
|
right={() => {
|
||||||
|
if (goalProgress.isNumeric()) {
|
||||||
|
return (
|
||||||
|
<View>
|
||||||
|
<Text>
|
||||||
|
{goalProgress.progress.toLocaleString()} /{" "}
|
||||||
|
{goalProgress.goal.target.value.toLocaleString()}{" "}
|
||||||
|
{goalProgress.goal.target.unit}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View>
|
||||||
|
<Text>{goalProgress.isCompleted() ? "true" : "false"}</Text>
|
||||||
|
</View>
|
||||||
|
)
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
40
presentation/react/components/HabitsMainPage/HabitsEmpty.tsx
Normal file
40
presentation/react/components/HabitsMainPage/HabitsEmpty.tsx
Normal file
@ -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 (
|
||||||
|
<View
|
||||||
|
style={{
|
||||||
|
flex: 1,
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "center",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Text variant="titleLarge">{"Let's begin by adding habits! 🤩"}</Text>
|
||||||
|
<Button
|
||||||
|
mode="contained"
|
||||||
|
style={{
|
||||||
|
marginTop: 16,
|
||||||
|
width: 250,
|
||||||
|
height: 40,
|
||||||
|
}}
|
||||||
|
onPress={() => {
|
||||||
|
router.push("/application/habits/new")
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Text
|
||||||
|
style={{
|
||||||
|
color: "white",
|
||||||
|
fontWeight: "bold",
|
||||||
|
fontSize: 16,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Create your first habit! 🚀
|
||||||
|
</Text>
|
||||||
|
</Button>
|
||||||
|
</View>
|
||||||
|
)
|
||||||
|
}
|
81
presentation/react/components/HabitsMainPage/HabitsList.tsx
Normal file
81
presentation/react/components/HabitsMainPage/HabitsList.tsx
Normal file
@ -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<HabitsListProps> = (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 <HabitsEmpty />
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ScrollView
|
||||||
|
showsVerticalScrollIndicator={false}
|
||||||
|
style={{
|
||||||
|
paddingHorizontal: 20,
|
||||||
|
width: Dimensions.get("window").width,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<List.Section>
|
||||||
|
{frequenciesFiltered.map((frequency) => {
|
||||||
|
return (
|
||||||
|
<List.Accordion
|
||||||
|
expanded={accordionExpanded[frequency]}
|
||||||
|
onPress={() => {
|
||||||
|
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 (
|
||||||
|
<HabitCard
|
||||||
|
habit={item.habit}
|
||||||
|
goalProgress={goalProgress}
|
||||||
|
key={item.habit.id}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</List.Accordion>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</List.Section>
|
||||||
|
</ScrollView>
|
||||||
|
)
|
||||||
|
}
|
@ -1,13 +1,9 @@
|
|||||||
import { useRouter } from "expo-router"
|
import { useMemo, useState } from "react"
|
||||||
import { useState } from "react"
|
import { Agenda } from "react-native-calendars"
|
||||||
import { Dimensions, ScrollView, View } from "react-native"
|
|
||||||
import { Button, List, Text } 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 type { HabitsTracker } from "@/domain/entities/HabitsTracker"
|
||||||
import { capitalize } from "@/presentation/presenters/utils/strings"
|
import { getISODate } from "@/utils/dates"
|
||||||
import { HabitCard } from "./HabitCard"
|
import { HabitsList } from "./HabitsList"
|
||||||
|
|
||||||
export interface HabitsMainPageProps {
|
export interface HabitsMainPageProps {
|
||||||
habitsTracker: HabitsTracker
|
habitsTracker: HabitsTracker
|
||||||
@ -16,91 +12,34 @@ export interface HabitsMainPageProps {
|
|||||||
export const HabitsMainPage: React.FC<HabitsMainPageProps> = (props) => {
|
export const HabitsMainPage: React.FC<HabitsMainPageProps> = (props) => {
|
||||||
const { habitsTracker } = props
|
const { habitsTracker } = props
|
||||||
|
|
||||||
const router = useRouter()
|
const today = useMemo(() => {
|
||||||
|
return new Date()
|
||||||
|
}, [])
|
||||||
|
const todayISO = getISODate(today)
|
||||||
|
|
||||||
const habitsByFrequency = GOAL_FREQUENCIES.filter((frequency) => {
|
const [selectedDate, setSelectedDate] = useState<Date>(today)
|
||||||
return habitsTracker.habitsHistory[frequency].length > 0
|
const selectedISODate = getISODate(selectedDate)
|
||||||
})
|
|
||||||
|
|
||||||
const [accordionExpanded, setAccordionExpanded] = useState<{
|
|
||||||
[key in GoalFrequency]: boolean
|
|
||||||
}>({
|
|
||||||
daily: true,
|
|
||||||
weekly: true,
|
|
||||||
monthly: true,
|
|
||||||
})
|
|
||||||
|
|
||||||
if (habitsByFrequency.length <= 0) {
|
|
||||||
return (
|
|
||||||
<View
|
|
||||||
style={{
|
|
||||||
flex: 1,
|
|
||||||
alignItems: "center",
|
|
||||||
justifyContent: "center",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Text variant="titleLarge">{"Let's begin by adding habits! 🤩"}</Text>
|
|
||||||
<Button
|
|
||||||
mode="contained"
|
|
||||||
style={{
|
|
||||||
marginTop: 16,
|
|
||||||
width: 250,
|
|
||||||
height: 40,
|
|
||||||
}}
|
|
||||||
onPress={() => {
|
|
||||||
router.push("/application/habits/new")
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Text
|
|
||||||
style={{
|
|
||||||
color: "white",
|
|
||||||
fontWeight: "bold",
|
|
||||||
fontSize: 16,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Create your first habit! 🚀
|
|
||||||
</Text>
|
|
||||||
</Button>
|
|
||||||
</View>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ScrollView
|
<Agenda
|
||||||
showsVerticalScrollIndicator={false}
|
firstDay={1}
|
||||||
style={{
|
showClosingKnob
|
||||||
paddingHorizontal: 20,
|
showOnlySelectedDayItems
|
||||||
width: Dimensions.get("window").width,
|
onDayPress={(date) => {
|
||||||
|
setSelectedDate(new Date(date.dateString))
|
||||||
}}
|
}}
|
||||||
>
|
markedDates={{
|
||||||
<List.Section>
|
[todayISO]: { marked: true },
|
||||||
{habitsByFrequency.map((frequency) => {
|
}}
|
||||||
|
selected={selectedISODate}
|
||||||
|
renderList={() => {
|
||||||
return (
|
return (
|
||||||
<List.Accordion
|
<HabitsList
|
||||||
expanded={accordionExpanded[frequency]}
|
habitsTracker={habitsTracker}
|
||||||
onPress={() => {
|
selectedDate={selectedDate}
|
||||||
setAccordionExpanded((old) => {
|
/>
|
||||||
return {
|
)
|
||||||
...old,
|
|
||||||
[frequency]: !old[frequency],
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}}
|
}}
|
||||||
key={frequency}
|
/>
|
||||||
title={capitalize(frequency)}
|
|
||||||
titleStyle={[
|
|
||||||
{
|
|
||||||
fontSize: 26,
|
|
||||||
},
|
|
||||||
]}
|
|
||||||
>
|
|
||||||
{habitsTracker.habitsHistory[frequency].map((item) => {
|
|
||||||
return <HabitCard habitHistory={item} key={item.habit.id} />
|
|
||||||
})}
|
|
||||||
</List.Accordion>
|
|
||||||
)
|
|
||||||
})}
|
|
||||||
</List.Section>
|
|
||||||
</ScrollView>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -2,11 +2,13 @@ import { useEffect, useState } from "react"
|
|||||||
|
|
||||||
import type { Presenter } from "@/presentation/presenters/_Presenter"
|
import type { Presenter } from "@/presentation/presenters/_Presenter"
|
||||||
|
|
||||||
export const usePresenterState = <S>(presenter: Presenter<S>): S => {
|
export const usePresenterState = <State>(
|
||||||
const [state, setState] = useState<S>(presenter.initialState)
|
presenter: Presenter<State>,
|
||||||
|
): State => {
|
||||||
|
const [state, setState] = useState<State>(presenter.initialState)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const presenterSubscription = (state: S): void => {
|
const presenterSubscription = (state: State): void => {
|
||||||
setState(state)
|
setState(state)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
35
utils/dates.ts
Normal file
35
utils/dates.ts
Normal file
@ -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,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
Reference in New Issue
Block a user