diff --git a/app/application/habits/[habitId]/index.tsx b/app/application/habits/[habitId]/index.tsx
index a7fcdec..02c162c 100644
--- a/app/application/habits/[habitId]/index.tsx
+++ b/app/application/habits/[habitId]/index.tsx
@@ -1,7 +1,6 @@
import { Redirect, useLocalSearchParams } from "expo-router"
-import { Text } from "react-native-paper"
-import { SafeAreaView } from "react-native-safe-area-context"
+import { HabitEditForm } from "@/presentation/react/components/HabitEditForm/HabitEditForm"
import { useHabitsTracker } from "@/presentation/react/contexts/HabitsTracker"
const HabitPage: React.FC = () => {
@@ -9,23 +8,13 @@ const HabitPage: React.FC = () => {
const { habitsTracker } = useHabitsTracker()
const habitHistory = habitsTracker.getHabitHistoryById(habitId as string)
+
if (habitHistory == null) {
return
}
return (
-
-
- Habit Page {habitId} {habitHistory.habit.name}
-
-
+
)
}
diff --git a/app/application/habits/new.tsx b/app/application/habits/new.tsx
index f2c5361..f410ab0 100644
--- a/app/application/habits/new.tsx
+++ b/app/application/habits/new.tsx
@@ -4,7 +4,7 @@ import { useAuthentication } from "@/presentation/react/contexts/Authentication"
const NewHabitPage: React.FC = () => {
const { user } = useAuthentication()
- if (user === null) {
+ if (user == null) {
return null
}
diff --git a/domain/entities/Habit.ts b/domain/entities/Habit.ts
index f111484..d80ba59 100644
--- a/domain/entities/Habit.ts
+++ b/domain/entities/Habit.ts
@@ -15,6 +15,9 @@ export const HabitCreateSchema = HabitSchema.extend({
}).omit({ id: true })
export type HabitCreateData = z.infer
+export const HabitEditSchema = HabitSchema.extend({})
+export type HabitEditData = z.infer
+
type HabitBase = z.infer
export interface HabitData extends HabitBase {
diff --git a/domain/entities/HabitsTracker.ts b/domain/entities/HabitsTracker.ts
index 834fb82..5712310 100644
--- a/domain/entities/HabitsTracker.ts
+++ b/domain/entities/HabitsTracker.ts
@@ -1,7 +1,6 @@
import type { GoalFrequency } from "./Goal"
import type { Habit } from "./Habit"
import { HabitHistory } from "./HabitHistory"
-import { HabitProgress } from "./HabitProgress"
export interface HabitsTrackerData {
habitsHistory: {
@@ -36,22 +35,30 @@ export class HabitsTracker implements HabitsTrackerData {
)
}
- public setHabitProgress(options: SetHabitProgressOptions): void {
- const { date, goalProgress, habitHistory } = options
- if (goalProgress.isBoolean()) {
- const currentHabitProgress = habitHistory.getProgressesByDate(date)[0]
- if (currentHabitProgress == null) {
- habitHistory.progressHistory = [
- ...habitHistory.progressHistory,
- new HabitProgress({
- date,
- goalProgress,
- habitId,
- id,
- }),
- ]
- }
+ // public setHabitProgress(options: SetHabitProgressOptions): void {
+ // const { date, goalProgress, habitHistory } = options
+ // if (goalProgress.isBoolean()) {
+ // const currentHabitProgress = habitHistory.getProgressesByDate(date)[0]
+ // if (currentHabitProgress == null) {
+ // habitHistory.progressHistory = [
+ // ...habitHistory.progressHistory,
+ // new HabitProgress({
+ // date,
+ // goalProgress,
+ // habitId,
+ // id,
+ // }),
+ // ]
+ // }
+ // }
+ // }
+
+ public editHabit(habit: Habit): void {
+ const habitHistory = this.getHabitHistoryById(habit.id)
+ if (habitHistory == null) {
+ return
}
+ habitHistory.habit = habit
}
public getAllHabitsHistory(): HabitHistory[] {
diff --git a/domain/repositories/HabitEdit.ts b/domain/repositories/HabitEdit.ts
new file mode 100644
index 0000000..3f5bddd
--- /dev/null
+++ b/domain/repositories/HabitEdit.ts
@@ -0,0 +1,9 @@
+import type { Habit, HabitEditData } from "../entities/Habit"
+
+export interface HabitEditOptions {
+ habitEditData: HabitEditData
+}
+
+export interface HabitEditRepository {
+ execute: (options: HabitEditOptions) => Promise
+}
diff --git a/domain/use-cases/HabitEdit.ts b/domain/use-cases/HabitEdit.ts
new file mode 100644
index 0000000..555b4e3
--- /dev/null
+++ b/domain/use-cases/HabitEdit.ts
@@ -0,0 +1,23 @@
+import type { Habit } from "../entities/Habit"
+import { HabitEditSchema } from "../entities/Habit"
+import type { HabitEditRepository } from "../repositories/HabitEdit"
+
+export interface HabitEditUseCaseDependencyOptions {
+ habitEditRepository: HabitEditRepository
+}
+
+export class HabitEditUseCase implements HabitEditUseCaseDependencyOptions {
+ public habitEditRepository: HabitEditRepository
+
+ public constructor(options: HabitEditUseCaseDependencyOptions) {
+ this.habitEditRepository = options.habitEditRepository
+ }
+
+ public async execute(data: unknown): Promise {
+ const habitEditData = await HabitEditSchema.parseAsync(data)
+ const habit = await this.habitEditRepository.execute({
+ habitEditData,
+ })
+ return habit
+ }
+}
diff --git a/infrastructure/instances.ts b/infrastructure/instances.ts
index 89b1690..8e92055 100644
--- a/infrastructure/instances.ts
+++ b/infrastructure/instances.ts
@@ -8,6 +8,8 @@ import { supabaseClient } from "./supabase/supabase"
import { AuthenticationPresenter } from "@/presentation/presenters/Authentication"
import { HabitCreateSupabaseRepository } from "./supabase/repositories/HabitCreate"
import { HabitCreateUseCase } from "@/domain/use-cases/HabitCreate"
+import { HabitEditSupabaseRepository } from "./supabase/repositories/HabitEdit"
+import { HabitEditUseCase } from "@/domain/use-cases/HabitEdit"
/**
* Repositories
@@ -25,6 +27,9 @@ const getHabitsByUserIdRepository = new GetHabitsByUserIdSupabaseRepository({
const habitCreateRepository = new HabitCreateSupabaseRepository({
supabaseClient,
})
+const habitEditRepository = new HabitEditSupabaseRepository({
+ supabaseClient,
+})
/**
* Use Cases
@@ -39,6 +44,9 @@ const retrieveHabitsTrackerUseCase = new RetrieveHabitsTrackerUseCase({
getHabitProgressHistoryRepository: getHabitProgressesRepository,
getHabitsByUserIdRepository,
})
+const habitEditUseCase = new HabitEditUseCase({
+ habitEditRepository,
+})
/**
* Presenters
@@ -49,4 +57,5 @@ export const authenticationPresenter = new AuthenticationPresenter({
export const habitsTrackerPresenter = new HabitsTrackerPresenter({
retrieveHabitsTrackerUseCase,
habitCreateUseCase,
+ habitEditUseCase,
})
diff --git a/infrastructure/supabase/repositories/HabitEdit.ts b/infrastructure/supabase/repositories/HabitEdit.ts
new file mode 100644
index 0000000..f482cc3
--- /dev/null
+++ b/infrastructure/supabase/repositories/HabitEdit.ts
@@ -0,0 +1,49 @@
+import { Habit } from "@/domain/entities/Habit"
+import type { HabitEditRepository } from "@/domain/repositories/HabitEdit"
+import { SupabaseRepository } from "./_SupabaseRepository"
+import { Goal } from "@/domain/entities/Goal"
+
+export class HabitEditSupabaseRepository
+ extends SupabaseRepository
+ implements HabitEditRepository
+{
+ public execute: HabitEditRepository["execute"] = async (options) => {
+ const { habitEditData } = options
+ const { data, error } = await this.supabaseClient
+ .from("habits")
+ .update({
+ name: habitEditData.name,
+ color: habitEditData.color,
+ icon: habitEditData.icon,
+ })
+ .eq("id", habitEditData.id)
+ .select("*")
+ const updatedHabit = data?.[0]
+ if (error != null || updatedHabit == null) {
+ throw new Error(error?.message ?? "Failed to edit habit.")
+ }
+ const habit = new Habit({
+ id: updatedHabit.id.toString(),
+ userId: updatedHabit.user_id.toString(),
+ name: updatedHabit.name,
+ icon: updatedHabit.icon,
+ goal: Goal.create({
+ frequency: updatedHabit.goal_frequency,
+ target:
+ updatedHabit.goal_target != null &&
+ updatedHabit.goal_target_unit != null
+ ? {
+ type: "numeric",
+ value: updatedHabit.goal_target,
+ unit: updatedHabit.goal_target_unit,
+ }
+ : {
+ type: "boolean",
+ },
+ }),
+ color: updatedHabit.color,
+ startDate: new Date(updatedHabit.start_date),
+ })
+ return habit
+ }
+}
diff --git a/presentation/presenters/HabitsTracker.ts b/presentation/presenters/HabitsTracker.ts
index b619559..9688adb 100644
--- a/presentation/presenters/HabitsTracker.ts
+++ b/presentation/presenters/HabitsTracker.ts
@@ -7,9 +7,10 @@ import type {
RetrieveHabitsTrackerUseCase,
RetrieveHabitsTrackerUseCaseOptions,
} from "@/domain/use-cases/RetrieveHabitsTracker"
-import type { HabitCreateData } from "@/domain/entities/Habit"
+import type { HabitCreateData, HabitEditData } from "@/domain/entities/Habit"
import { getErrorsFieldsFromZodError } from "../../utils/zod"
import type { HabitCreateUseCase } from "@/domain/use-cases/HabitCreate"
+import type { HabitEditUseCase } from "@/domain/use-cases/HabitEdit"
export interface HabitsTrackerPresenterState {
habitsTracker: HabitsTracker
@@ -25,11 +26,20 @@ export interface HabitsTrackerPresenterState {
global: ErrorGlobal
}
}
+
+ habitEdit: {
+ state: FetchState
+ errors: {
+ fields: Array
+ global: ErrorGlobal
+ }
+ }
}
export interface HabitsTrackerPresenterOptions {
retrieveHabitsTrackerUseCase: RetrieveHabitsTrackerUseCase
habitCreateUseCase: HabitCreateUseCase
+ habitEditUseCase: HabitEditUseCase
}
export class HabitsTrackerPresenter
@@ -38,9 +48,14 @@ export class HabitsTrackerPresenter
{
public retrieveHabitsTrackerUseCase: RetrieveHabitsTrackerUseCase
public habitCreateUseCase: HabitCreateUseCase
+ public habitEditUseCase: HabitEditUseCase
public constructor(options: HabitsTrackerPresenterOptions) {
- const { retrieveHabitsTrackerUseCase, habitCreateUseCase } = options
+ const {
+ retrieveHabitsTrackerUseCase,
+ habitCreateUseCase,
+ habitEditUseCase,
+ } = options
const habitsTracker = HabitsTracker.default()
super({
habitsTracker,
@@ -52,9 +67,17 @@ export class HabitsTrackerPresenter
global: null,
},
},
+ habitEdit: {
+ state: "idle",
+ errors: {
+ fields: [],
+ global: null,
+ },
+ },
})
this.retrieveHabitsTrackerUseCase = retrieveHabitsTrackerUseCase
this.habitCreateUseCase = habitCreateUseCase
+ this.habitEditUseCase = habitEditUseCase
}
public async habitCreate(data: unknown): Promise {
@@ -86,6 +109,35 @@ export class HabitsTrackerPresenter
}
}
+ public async habitEdit(data: unknown): Promise {
+ try {
+ this.setState((state) => {
+ state.habitEdit.state = "loading"
+ state.habitEdit.errors = {
+ fields: [],
+ global: null,
+ }
+ })
+ const habit = await this.habitEditUseCase.execute(data)
+ this.setState((state) => {
+ state.habitEdit.state = "success"
+ state.habitsTracker.editHabit(habit)
+ })
+ return "success"
+ } catch (error) {
+ this.setState((state) => {
+ state.habitEdit.state = "error"
+ if (error instanceof ZodError) {
+ state.habitEdit.errors.fields =
+ getErrorsFieldsFromZodError(error)
+ } else {
+ state.habitEdit.errors.global = "unknown"
+ }
+ })
+ return "error"
+ }
+ }
+
public async retrieveHabitsTracker(
options: RetrieveHabitsTrackerUseCaseOptions,
): Promise {
diff --git a/presentation/react/components/HabitEditForm/HabitEditForm.tsx b/presentation/react/components/HabitEditForm/HabitEditForm.tsx
new file mode 100644
index 0000000..be69d7f
--- /dev/null
+++ b/presentation/react/components/HabitEditForm/HabitEditForm.tsx
@@ -0,0 +1,154 @@
+import { zodResolver } from "@hookform/resolvers/zod"
+import { useState } from "react"
+import { Controller, useForm } from "react-hook-form"
+import { ScrollView, StyleSheet } from "react-native"
+import { Button, HelperText, Snackbar, TextInput } from "react-native-paper"
+import { SafeAreaView } from "react-native-safe-area-context"
+import ColorPicker, {
+ HueSlider,
+ Panel1,
+ Preview,
+} from "reanimated-color-picker"
+
+import type { Habit, HabitEditData } from "@/domain/entities/Habit"
+import { HabitEditSchema } from "@/domain/entities/Habit"
+import { useHabitsTracker } from "../../contexts/HabitsTracker"
+
+export interface HabitEditFormProps {
+ habit: Habit
+}
+
+export const HabitEditForm: React.FC = ({ habit }) => {
+ const { habitEdit, habitsTrackerPresenter } = useHabitsTracker()
+
+ const {
+ control,
+ handleSubmit,
+ formState: { errors },
+ } = useForm({
+ mode: "onChange",
+ resolver: zodResolver(HabitEditSchema),
+ defaultValues: {
+ id: habit.id,
+ userId: habit.userId,
+ name: habit.name,
+ color: habit.color,
+ icon: habit.icon,
+ },
+ })
+
+ const [isVisibleSnackbar, setIsVisibleSnackbar] = useState(false)
+
+ const onDismissSnackbar = (): void => {
+ setIsVisibleSnackbar(false)
+ }
+
+ const onSubmit = async (data: HabitEditData): Promise => {
+ await habitsTrackerPresenter.habitEdit(data)
+ setIsVisibleSnackbar(true)
+ }
+
+ return (
+
+
+ {
+ return (
+ <>
+
+ {errors.name != null ? (
+
+ {errors.name.type === "too_big"
+ ? "Name is too long"
+ : "Name is required"}
+
+ ) : null}
+ >
+ )
+ }}
+ name="name"
+ />
+
+ {
+ return (
+ {
+ onChange(value.hex)
+ }}
+ >
+
+
+
+
+ )
+ }}
+ name="color"
+ />
+
+ {
+ return (
+
+ )
+ }}
+ name="icon"
+ />
+
+
+
+
+
+ ✅ Habit Saved successfully!
+
+
+ )
+}
+
+const styles = StyleSheet.create({
+ spacing: {
+ marginVertical: 16,
+ },
+})