diff --git a/app/application/habits/[habitId]/index.tsx b/app/application/habits/[habitId]/index.tsx
index a7fcdec..5a03693 100644
--- a/app/application/habits/[habitId]/index.tsx
+++ b/app/application/habits/[habitId]/index.tsx
@@ -3,10 +3,17 @@ import { Text } from "react-native-paper"
import { SafeAreaView } from "react-native-safe-area-context"
import { useHabitsTracker } from "@/presentation/react/contexts/HabitsTracker"
+import { HabitEditForm } from "@/presentation/react/components/HabitEditForm/HabitEditForm"
+import { useAuthentication } from "@/presentation/react/contexts/Authentication"
const HabitPage: React.FC = () => {
const { habitId } = useLocalSearchParams()
const { habitsTracker } = useHabitsTracker()
+ const { user } = useAuthentication()
+
+ if (user === null) {
+ return null
+ }
const habitHistory = habitsTracker.getHabitHistoryById(habitId as string)
if (habitHistory == null) {
@@ -25,6 +32,7 @@ const HabitPage: React.FC = () => {
Habit Page {habitId} {habitHistory.habit.name}
+
)
}
diff --git a/domain/entities/Goal.ts b/domain/entities/Goal.ts
index ded0b30..d55fcbf 100644
--- a/domain/entities/Goal.ts
+++ b/domain/entities/Goal.ts
@@ -33,8 +33,22 @@ export const GoalCreateSchema = z.object({
]),
})
+export const GoalEditSchema = z.object({
+ frequency: goalFrequencyZod,
+ target: z.discriminatedUnion("type", [
+ z.object({ type: z.literal("boolean") }),
+ z.object({
+ type: z.literal("numeric"),
+ value: z.number().int().min(0),
+ unit: z.string().min(1),
+ }),
+ ]),
+})
+
export type GoalCreateData = z.infer
+export type GoalEditData = z.infer
+
interface GoalBase {
frequency: GoalFrequency
}
diff --git a/domain/entities/Habit.ts b/domain/entities/Habit.ts
index f111484..e9de6c3 100644
--- a/domain/entities/Habit.ts
+++ b/domain/entities/Habit.ts
@@ -15,6 +15,11 @@ export const HabitCreateSchema = HabitSchema.extend({
}).omit({ id: true })
export type HabitCreateData = z.infer
+export const HabitEditSchema = HabitSchema.extend({
+ goal: GoalCreateSchema,
+})
+export type HabitEditData = z.infer
+
type HabitBase = z.infer
export interface HabitData extends HabitBase {
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/supabase/repositories/HabitEdit.ts b/infrastructure/supabase/repositories/HabitEdit.ts
new file mode 100644
index 0000000..9020098
--- /dev/null
+++ b/infrastructure/supabase/repositories/HabitEdit.ts
@@ -0,0 +1,43 @@
+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({
+ id: Number(habitEditData.id),
+ name: habitEditData.name,
+ color: habitEditData.color,
+ icon: habitEditData.icon,
+ goal_frequency: habitEditData.goal.frequency,
+ ...(habitEditData.goal.target.type === "numeric"
+ ? {
+ goal_target: habitEditData.goal.target.value,
+ goal_target_unit: habitEditData.goal.target.unit,
+ }
+ : {}),
+ })
+ .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(habitEditData.goal),
+ 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..7e0d923 100644
--- a/presentation/presenters/HabitsTracker.ts
+++ b/presentation/presenters/HabitsTracker.ts
@@ -25,6 +25,14 @@ export interface HabitsTrackerPresenterState {
global: ErrorGlobal
}
}
+
+ habitEdit: {
+ state: FetchState
+ errors: {
+ fields: Array
+ global: ErrorGlobal
+ }
+ }
}
export interface HabitsTrackerPresenterOptions {
@@ -52,6 +60,13 @@ export class HabitsTrackerPresenter
global: null,
},
},
+ habitEdit: {
+ state: "idle",
+ errors: {
+ fields: [],
+ global: null,
+ },
+ },
})
this.retrieveHabitsTrackerUseCase = retrieveHabitsTrackerUseCase
this.habitCreateUseCase = habitCreateUseCase
@@ -86,6 +101,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.habitCreateUseCase.execute(data)
+ this.setState((state) => {
+ state.habitEdit.state = "success"
+ state.habitsTracker.addHabit(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..b26d6e2
--- /dev/null
+++ b/presentation/react/components/HabitEditForm/HabitEditForm.tsx
@@ -0,0 +1,163 @@
+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 type { User } from "@/domain/entities/User"
+import { useHabitsTracker } from "../../contexts/HabitsTracker"
+
+export interface HabitEditFormProps {
+ user: User
+ habit: Habit
+}
+
+export const HabitEditForm: React.FC = ({ user }) => {
+ const { habitEdit, habitsTrackerPresenter } = useHabitsTracker()
+
+ const {
+ control,
+ handleSubmit,
+ reset,
+ formState: { errors },
+ } = useForm({
+ mode: "onChange",
+ resolver: zodResolver(HabitEditSchema),
+ defaultValues: {
+ userId: user.id,
+ name: "",
+ color: "#006CFF",
+ icon: "lightbulb",
+ goal: {
+ frequency: "daily",
+ target: {
+ type: "boolean",
+ },
+ },
+ },
+ })
+
+ const [isVisibleSnackbar, setIsVisibleSnackbar] = useState(false)
+
+ const onDismissSnackbar = (): void => {
+ setIsVisibleSnackbar(false)
+ }
+
+ const onSubmit = async (data: HabitEditData): Promise => {
+ await habitsTrackerPresenter.habitEdit(data)
+ setIsVisibleSnackbar(true)
+ reset()
+ }
+
+ 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 created successfully!
+
+
+ )
+}
+
+const styles = StyleSheet.create({
+ spacing: {
+ marginVertical: 16,
+ },
+})