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, + }, +})