feat: habit create use case
This commit is contained in:
parent
a2d187a27a
commit
e4fcb1894c
@ -6,6 +6,7 @@ import {
|
|||||||
} from "react-native-paper"
|
} from "react-native-paper"
|
||||||
import { StatusBar } from "expo-status-bar"
|
import { StatusBar } from "expo-status-bar"
|
||||||
import { useEffect } from "react"
|
import { useEffect } from "react"
|
||||||
|
import { GestureHandlerRootView } from "react-native-gesture-handler"
|
||||||
|
|
||||||
import { HabitsTrackerProvider } from "@/presentation/react/contexts/HabitsTracker"
|
import { HabitsTrackerProvider } from "@/presentation/react/contexts/HabitsTracker"
|
||||||
import {
|
import {
|
||||||
@ -61,7 +62,9 @@ const RootLayout: React.FC = () => {
|
|||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
<GestureHandlerRootView style={{ flex: 1 }}>
|
||||||
<StackLayout />
|
<StackLayout />
|
||||||
|
</GestureHandlerRootView>
|
||||||
|
|
||||||
<StatusBar style="dark" />
|
<StatusBar style="dark" />
|
||||||
</PaperProvider>
|
</PaperProvider>
|
||||||
|
@ -30,7 +30,8 @@ const HabitsPage: React.FC = () => {
|
|||||||
) : retrieveHabitsTracker.state === "error" ? (
|
) : retrieveHabitsTracker.state === "error" ? (
|
||||||
<>
|
<>
|
||||||
<Text variant="titleLarge">
|
<Text variant="titleLarge">
|
||||||
Error: There was an issue while retrieving habits, please try again.
|
Error: There was an issue while retrieving habits, please try again
|
||||||
|
later.
|
||||||
</Text>
|
</Text>
|
||||||
<Button
|
<Button
|
||||||
mode="contained"
|
mode="contained"
|
||||||
|
@ -12,8 +12,8 @@ export const GOAL_FREQUENCIES = GOAL_FREQUENCIES_ZOD.map((frequency) => {
|
|||||||
export type GoalFrequency = (typeof GOAL_FREQUENCIES)[number]
|
export type GoalFrequency = (typeof GOAL_FREQUENCIES)[number]
|
||||||
|
|
||||||
export const GOAL_TYPES_ZOD = [
|
export const GOAL_TYPES_ZOD = [
|
||||||
z.literal("numeric"),
|
|
||||||
z.literal("boolean"),
|
z.literal("boolean"),
|
||||||
|
z.literal("numeric"),
|
||||||
] as const
|
] as const
|
||||||
export const goalTypeZod = z.union(GOAL_TYPES_ZOD)
|
export const goalTypeZod = z.union(GOAL_TYPES_ZOD)
|
||||||
export const GOAL_TYPES = GOAL_TYPES_ZOD.map((type) => {
|
export const GOAL_TYPES = GOAL_TYPES_ZOD.map((type) => {
|
||||||
@ -51,6 +51,16 @@ export abstract class Goal implements GoalBase {
|
|||||||
this.frequency = frequency
|
this.frequency = frequency
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static create(options: GoalCreateData): Goal {
|
||||||
|
if (options.target.type === "boolean") {
|
||||||
|
return new GoalBoolean(options)
|
||||||
|
}
|
||||||
|
return new GoalNumeric({
|
||||||
|
frequency: options.frequency,
|
||||||
|
target: options.target,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
public static isNumeric(goal: Goal): goal is GoalNumeric {
|
public static isNumeric(goal: Goal): goal is GoalNumeric {
|
||||||
return goal.type === "numeric"
|
return goal.type === "numeric"
|
||||||
}
|
}
|
||||||
|
@ -15,15 +15,15 @@ export const HabitCreateSchema = HabitSchema.extend({
|
|||||||
}).omit({ id: true })
|
}).omit({ id: true })
|
||||||
export type HabitCreateData = z.infer<typeof HabitCreateSchema>
|
export type HabitCreateData = z.infer<typeof HabitCreateSchema>
|
||||||
|
|
||||||
type HabitDataBase = z.infer<typeof HabitSchema>
|
type HabitBase = z.infer<typeof HabitSchema>
|
||||||
|
|
||||||
export interface HabitData extends HabitDataBase {
|
export interface HabitData extends HabitBase {
|
||||||
goal: Goal
|
goal: Goal
|
||||||
startDate: Date
|
startDate: Date
|
||||||
endDate?: Date
|
endDate?: Date
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface HabitJSON extends HabitDataBase {
|
export interface HabitJSON extends HabitBase {
|
||||||
goal: GoalBaseJSON
|
goal: GoalBaseJSON
|
||||||
startDate: string
|
startDate: string
|
||||||
endDate?: string
|
endDate?: string
|
||||||
|
@ -3,16 +3,16 @@ import type { Habit } from "./Habit"
|
|||||||
import type { EntityData } from "./_Entity"
|
import type { EntityData } from "./_Entity"
|
||||||
import { Entity } from "./_Entity"
|
import { Entity } from "./_Entity"
|
||||||
|
|
||||||
interface HabitProgressDataBase extends EntityData {
|
interface HabitProgressBase extends EntityData {
|
||||||
habitId: Habit["id"]
|
habitId: Habit["id"]
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface HabitProgressData extends HabitProgressDataBase {
|
export interface HabitProgressData extends HabitProgressBase {
|
||||||
goalProgress: GoalProgress
|
goalProgress: GoalProgress
|
||||||
date: Date
|
date: Date
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface HabitProgressJSON extends HabitProgressDataBase {
|
export interface HabitProgressJSON extends HabitProgressBase {
|
||||||
goalProgress: GoalProgressBase
|
goalProgress: GoalProgressBase
|
||||||
date: string
|
date: string
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import type { GoalFrequency } from "./Goal"
|
import type { GoalFrequency } from "./Goal"
|
||||||
import type { HabitHistory } from "./HabitHistory"
|
import type { Habit } from "./Habit"
|
||||||
|
import { HabitHistory } from "./HabitHistory"
|
||||||
|
|
||||||
export interface HabitsTrackerData {
|
export interface HabitsTrackerData {
|
||||||
habitsHistory: {
|
habitsHistory: {
|
||||||
@ -24,4 +25,13 @@ export class HabitsTracker implements HabitsTrackerData {
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public addHabit(habit: Habit): void {
|
||||||
|
this.habitsHistory[habit.goal.frequency].push(
|
||||||
|
new HabitHistory({
|
||||||
|
habit,
|
||||||
|
progressHistory: [],
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
9
domain/repositories/HabitCreate.ts
Normal file
9
domain/repositories/HabitCreate.ts
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
import type { Habit, HabitCreateData } from "../entities/Habit"
|
||||||
|
|
||||||
|
export interface HabitCreateOptions {
|
||||||
|
habitCreateData: HabitCreateData
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface HabitCreateRepository {
|
||||||
|
execute: (options: HabitCreateOptions) => Promise<Habit>
|
||||||
|
}
|
23
domain/use-cases/HabitCreate.ts
Normal file
23
domain/use-cases/HabitCreate.ts
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
import type { Habit } from "../entities/Habit"
|
||||||
|
import { HabitCreateSchema } from "../entities/Habit"
|
||||||
|
import type { HabitCreateRepository } from "../repositories/HabitCreate"
|
||||||
|
|
||||||
|
export interface HabitCreateUseCaseDependencyOptions {
|
||||||
|
habitCreateRepository: HabitCreateRepository
|
||||||
|
}
|
||||||
|
|
||||||
|
export class HabitCreateUseCase implements HabitCreateUseCaseDependencyOptions {
|
||||||
|
public habitCreateRepository: HabitCreateRepository
|
||||||
|
|
||||||
|
public constructor(options: HabitCreateUseCaseDependencyOptions) {
|
||||||
|
this.habitCreateRepository = options.habitCreateRepository
|
||||||
|
}
|
||||||
|
|
||||||
|
public async execute(data: unknown): Promise<Habit> {
|
||||||
|
const habitCreateData = await HabitCreateSchema.parseAsync(data)
|
||||||
|
const habit = await this.habitCreateRepository.execute({
|
||||||
|
habitCreateData,
|
||||||
|
})
|
||||||
|
return habit
|
||||||
|
}
|
||||||
|
}
|
@ -6,6 +6,8 @@ import { GetHabitProgressHistorySupabaseRepository } from "./supabase/repositori
|
|||||||
import { GetHabitsByUserIdSupabaseRepository } from "./supabase/repositories/GetHabitsByUserId"
|
import { GetHabitsByUserIdSupabaseRepository } from "./supabase/repositories/GetHabitsByUserId"
|
||||||
import { supabaseClient } from "./supabase/supabase"
|
import { supabaseClient } from "./supabase/supabase"
|
||||||
import { AuthenticationPresenter } from "@/presentation/presenters/Authentication"
|
import { AuthenticationPresenter } from "@/presentation/presenters/Authentication"
|
||||||
|
import { HabitCreateSupabaseRepository } from "./supabase/repositories/HabitCreate"
|
||||||
|
import { HabitCreateUseCase } from "@/domain/use-cases/HabitCreate"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Repositories
|
* Repositories
|
||||||
@ -20,6 +22,9 @@ const getHabitProgressesRepository =
|
|||||||
const getHabitsByUserIdRepository = new GetHabitsByUserIdSupabaseRepository({
|
const getHabitsByUserIdRepository = new GetHabitsByUserIdSupabaseRepository({
|
||||||
supabaseClient,
|
supabaseClient,
|
||||||
})
|
})
|
||||||
|
const habitCreateRepository = new HabitCreateSupabaseRepository({
|
||||||
|
supabaseClient,
|
||||||
|
})
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Use Cases
|
* Use Cases
|
||||||
@ -27,6 +32,9 @@ const getHabitsByUserIdRepository = new GetHabitsByUserIdSupabaseRepository({
|
|||||||
const authenticationUseCase = new AuthenticationUseCase({
|
const authenticationUseCase = new AuthenticationUseCase({
|
||||||
authenticationRepository,
|
authenticationRepository,
|
||||||
})
|
})
|
||||||
|
const habitCreateUseCase = new HabitCreateUseCase({
|
||||||
|
habitCreateRepository,
|
||||||
|
})
|
||||||
const retrieveHabitsTrackerUseCase = new RetrieveHabitsTrackerUseCase({
|
const retrieveHabitsTrackerUseCase = new RetrieveHabitsTrackerUseCase({
|
||||||
getHabitProgressHistoryRepository: getHabitProgressesRepository,
|
getHabitProgressHistoryRepository: getHabitProgressesRepository,
|
||||||
getHabitsByUserIdRepository,
|
getHabitsByUserIdRepository,
|
||||||
@ -40,4 +48,5 @@ export const authenticationPresenter = new AuthenticationPresenter({
|
|||||||
})
|
})
|
||||||
export const habitsTrackerPresenter = new HabitsTrackerPresenter({
|
export const habitsTrackerPresenter = new HabitsTrackerPresenter({
|
||||||
retrieveHabitsTrackerUseCase,
|
retrieveHabitsTrackerUseCase,
|
||||||
|
habitCreateUseCase,
|
||||||
})
|
})
|
||||||
|
42
infrastructure/supabase/repositories/HabitCreate.ts
Normal file
42
infrastructure/supabase/repositories/HabitCreate.ts
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
import { Habit } from "@/domain/entities/Habit"
|
||||||
|
import type { HabitCreateRepository } from "@/domain/repositories/HabitCreate"
|
||||||
|
import { SupabaseRepository } from "./_SupabaseRepository"
|
||||||
|
import { Goal } from "@/domain/entities/Goal"
|
||||||
|
|
||||||
|
export class HabitCreateSupabaseRepository
|
||||||
|
extends SupabaseRepository
|
||||||
|
implements HabitCreateRepository
|
||||||
|
{
|
||||||
|
public execute: HabitCreateRepository["execute"] = async (options) => {
|
||||||
|
const { habitCreateData } = options
|
||||||
|
const { data, error } = await this.supabaseClient
|
||||||
|
.from("habits")
|
||||||
|
.insert({
|
||||||
|
name: habitCreateData.name,
|
||||||
|
color: habitCreateData.color,
|
||||||
|
icon: habitCreateData.icon,
|
||||||
|
goal_frequency: habitCreateData.goal.frequency,
|
||||||
|
...(habitCreateData.goal.target.type === "numeric"
|
||||||
|
? {
|
||||||
|
goal_target: habitCreateData.goal.target.value,
|
||||||
|
goal_target_unit: habitCreateData.goal.target.unit,
|
||||||
|
}
|
||||||
|
: {}),
|
||||||
|
})
|
||||||
|
.select("*")
|
||||||
|
const insertedHabit = data?.[0]
|
||||||
|
if (error != null || insertedHabit == null) {
|
||||||
|
throw new Error(error?.message ?? "Failed to create habit.")
|
||||||
|
}
|
||||||
|
const habit = new Habit({
|
||||||
|
id: insertedHabit.id.toString(),
|
||||||
|
userId: insertedHabit.user_id.toString(),
|
||||||
|
name: insertedHabit.name,
|
||||||
|
icon: insertedHabit.icon,
|
||||||
|
goal: Goal.create(habitCreateData.goal),
|
||||||
|
color: insertedHabit.color,
|
||||||
|
startDate: new Date(insertedHabit.start_date),
|
||||||
|
})
|
||||||
|
return habit
|
||||||
|
}
|
||||||
|
}
|
@ -262,3 +262,6 @@ VALUES
|
|||||||
timezone('utc' :: text, NOW()),
|
timezone('utc' :: text, NOW()),
|
||||||
4733
|
4733
|
||||||
);
|
);
|
||||||
|
|
||||||
|
-- SELECT setval('habits_id_seq', (SELECT coalesce(MAX(id) + 1, 1) FROM habits), false);
|
||||||
|
-- SELECT setval('habits_progresses_id_seq', (SELECT coalesce(MAX(id) + 1, 1) FROM habits_progresses), false);
|
||||||
|
@ -5,8 +5,12 @@ import AsyncStorage from "@react-native-async-storage/async-storage"
|
|||||||
|
|
||||||
import type { Database } from "./supabase-types"
|
import type { Database } from "./supabase-types"
|
||||||
|
|
||||||
const SUPABASE_URL = process.env["EXPO_PUBLIC_SUPABASE_URL"] ?? ""
|
const SUPABASE_URL =
|
||||||
const SUPABASE_ANON_KEY = process.env["EXPO_PUBLIC_SUPABASE_ANON_KEY"] ?? ""
|
process.env["EXPO_PUBLIC_SUPABASE_URL"] ??
|
||||||
|
"https://wjtwtzxreersqfvfgxrz.supabase.co"
|
||||||
|
const SUPABASE_ANON_KEY =
|
||||||
|
process.env["EXPO_PUBLIC_SUPABASE_ANON_KEY"] ??
|
||||||
|
"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6IndqdHd0enhyZWVyc3FmdmZneHJ6Iiwicm9sZSI6ImFub24iLCJpYXQiOjE3MTExNDcyNzAsImV4cCI6MjAyNjcyMzI3MH0.AglONhsMvmcCRkqwrZsB4Ws9u3o1FAbLlpKJmqeUv8I"
|
||||||
|
|
||||||
export const supabaseClient = createClient<Database>(
|
export const supabaseClient = createClient<Database>(
|
||||||
SUPABASE_URL,
|
SUPABASE_URL,
|
||||||
|
596
package-lock.json
generated
596
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
26
package.json
26
package.json
@ -23,7 +23,7 @@
|
|||||||
"@hookform/resolvers": "3.3.4",
|
"@hookform/resolvers": "3.3.4",
|
||||||
"@react-native-async-storage/async-storage": "1.21.0",
|
"@react-native-async-storage/async-storage": "1.21.0",
|
||||||
"@react-navigation/native": "6.1.16",
|
"@react-navigation/native": "6.1.16",
|
||||||
"@supabase/supabase-js": "2.39.8",
|
"@supabase/supabase-js": "2.42.0",
|
||||||
"expo": "50.0.14",
|
"expo": "50.0.14",
|
||||||
"expo-font": "11.10.3",
|
"expo-font": "11.10.3",
|
||||||
"expo-linking": "6.2.2",
|
"expo-linking": "6.2.2",
|
||||||
@ -35,13 +35,13 @@
|
|||||||
"immer": "10.0.4",
|
"immer": "10.0.4",
|
||||||
"react": "18.2.0",
|
"react": "18.2.0",
|
||||||
"react-dom": "18.2.0",
|
"react-dom": "18.2.0",
|
||||||
"react-hook-form": "7.51.1",
|
"react-hook-form": "7.51.2",
|
||||||
"react-native": "0.73.6",
|
"react-native": "0.73.6",
|
||||||
"react-native-calendars": "1.1304.1",
|
"react-native-calendars": "1.1304.1",
|
||||||
"react-native-elements": "3.4.3",
|
"react-native-elements": "3.4.3",
|
||||||
"react-native-gesture-handler": "2.16.0",
|
"react-native-gesture-handler": "2.14.1",
|
||||||
"react-native-paper": "5.12.3",
|
"react-native-paper": "5.12.3",
|
||||||
"react-native-reanimated": "~3.6.2",
|
"react-native-reanimated": "3.6.3",
|
||||||
"react-native-safe-area-context": "4.8.2",
|
"react-native-safe-area-context": "4.8.2",
|
||||||
"react-native-screens": "3.29.0",
|
"react-native-screens": "3.29.0",
|
||||||
"react-native-url-polyfill": "2.0.0",
|
"react-native-url-polyfill": "2.0.0",
|
||||||
@ -51,18 +51,18 @@
|
|||||||
"zod": "3.22.4"
|
"zod": "3.22.4"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/core": "7.24.3",
|
"@babel/core": "7.24.4",
|
||||||
"@commitlint/cli": "19.1.0",
|
"@commitlint/cli": "19.1.0",
|
||||||
"@commitlint/config-conventional": "19.1.0",
|
"@commitlint/config-conventional": "19.1.0",
|
||||||
"@testing-library/react-native": "12.4.4",
|
"@testing-library/react-native": "12.4.5",
|
||||||
"@total-typescript/ts-reset": "0.5.1",
|
"@total-typescript/ts-reset": "0.5.1",
|
||||||
"@tsconfig/strictest": "2.0.3",
|
"@tsconfig/strictest": "2.0.5",
|
||||||
"@types/jest": "29.5.12",
|
"@types/jest": "29.5.12",
|
||||||
"@types/node": "20.11.30",
|
"@types/node": "20.12.4",
|
||||||
"@types/react": "18.2.69",
|
"@types/react": "18.2.74",
|
||||||
"@types/react-test-renderer": "18.0.7",
|
"@types/react-test-renderer": "18.0.7",
|
||||||
"@typescript-eslint/eslint-plugin": "7.3.1",
|
"@typescript-eslint/eslint-plugin": "7.5.0",
|
||||||
"@typescript-eslint/parser": "7.3.1",
|
"@typescript-eslint/parser": "7.5.0",
|
||||||
"eslint": "8.57.0",
|
"eslint": "8.57.0",
|
||||||
"eslint-config-conventions": "14.1.0",
|
"eslint-config-conventions": "14.1.0",
|
||||||
"eslint-config-prettier": "9.1.0",
|
"eslint-config-prettier": "9.1.0",
|
||||||
@ -79,7 +79,7 @@
|
|||||||
"jest-junit": "16.0.0",
|
"jest-junit": "16.0.0",
|
||||||
"lint-staged": "15.2.2",
|
"lint-staged": "15.2.2",
|
||||||
"react-test-renderer": "18.2.0",
|
"react-test-renderer": "18.2.0",
|
||||||
"supabase": "1.150.0",
|
"supabase": "1.153.4",
|
||||||
"typescript": "5.4.3"
|
"typescript": "5.4.4"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,10 +1,15 @@
|
|||||||
|
import { ZodError } from "zod"
|
||||||
|
|
||||||
import { HabitsTracker } from "@/domain/entities/HabitsTracker"
|
import { HabitsTracker } from "@/domain/entities/HabitsTracker"
|
||||||
import type { FetchState } from "./_Presenter"
|
import type { ErrorGlobal, FetchState } from "./_Presenter"
|
||||||
import { Presenter } from "./_Presenter"
|
import { Presenter } from "./_Presenter"
|
||||||
import type {
|
import type {
|
||||||
RetrieveHabitsTrackerUseCase,
|
RetrieveHabitsTrackerUseCase,
|
||||||
RetrieveHabitsTrackerUseCaseOptions,
|
RetrieveHabitsTrackerUseCaseOptions,
|
||||||
} from "@/domain/use-cases/RetrieveHabitsTracker"
|
} from "@/domain/use-cases/RetrieveHabitsTracker"
|
||||||
|
import type { HabitCreateData } from "@/domain/entities/Habit"
|
||||||
|
import { getErrorsFieldsFromZodError } from "./utils/zod"
|
||||||
|
import type { HabitCreateUseCase } from "@/domain/use-cases/HabitCreate"
|
||||||
|
|
||||||
export interface HabitsTrackerPresenterState {
|
export interface HabitsTrackerPresenterState {
|
||||||
habitsTracker: HabitsTracker
|
habitsTracker: HabitsTracker
|
||||||
@ -12,10 +17,19 @@ export interface HabitsTrackerPresenterState {
|
|||||||
retrieveHabitsTracker: {
|
retrieveHabitsTracker: {
|
||||||
state: FetchState
|
state: FetchState
|
||||||
}
|
}
|
||||||
|
|
||||||
|
habitCreate: {
|
||||||
|
state: FetchState
|
||||||
|
errors: {
|
||||||
|
fields: Array<keyof HabitCreateData>
|
||||||
|
global: ErrorGlobal
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface HabitsTrackerPresenterOptions {
|
export interface HabitsTrackerPresenterOptions {
|
||||||
retrieveHabitsTrackerUseCase: RetrieveHabitsTrackerUseCase
|
retrieveHabitsTrackerUseCase: RetrieveHabitsTrackerUseCase
|
||||||
|
habitCreateUseCase: HabitCreateUseCase
|
||||||
}
|
}
|
||||||
|
|
||||||
export class HabitsTrackerPresenter
|
export class HabitsTrackerPresenter
|
||||||
@ -23,12 +37,53 @@ export class HabitsTrackerPresenter
|
|||||||
implements HabitsTrackerPresenterOptions
|
implements HabitsTrackerPresenterOptions
|
||||||
{
|
{
|
||||||
public retrieveHabitsTrackerUseCase: RetrieveHabitsTrackerUseCase
|
public retrieveHabitsTrackerUseCase: RetrieveHabitsTrackerUseCase
|
||||||
|
public habitCreateUseCase: HabitCreateUseCase
|
||||||
|
|
||||||
public constructor(options: HabitsTrackerPresenterOptions) {
|
public constructor(options: HabitsTrackerPresenterOptions) {
|
||||||
const { retrieveHabitsTrackerUseCase } = options
|
const { retrieveHabitsTrackerUseCase, habitCreateUseCase } = options
|
||||||
const habitsTracker = HabitsTracker.default()
|
const habitsTracker = HabitsTracker.default()
|
||||||
super({ habitsTracker, retrieveHabitsTracker: { state: "idle" } })
|
super({
|
||||||
|
habitsTracker,
|
||||||
|
retrieveHabitsTracker: { state: "idle" },
|
||||||
|
habitCreate: {
|
||||||
|
state: "idle",
|
||||||
|
errors: {
|
||||||
|
fields: [],
|
||||||
|
global: null,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
this.retrieveHabitsTrackerUseCase = retrieveHabitsTrackerUseCase
|
this.retrieveHabitsTrackerUseCase = retrieveHabitsTrackerUseCase
|
||||||
|
this.habitCreateUseCase = habitCreateUseCase
|
||||||
|
}
|
||||||
|
|
||||||
|
public async habitCreate(data: unknown): Promise<FetchState> {
|
||||||
|
try {
|
||||||
|
this.setState((state) => {
|
||||||
|
state.habitCreate.state = "loading"
|
||||||
|
state.habitCreate.errors = {
|
||||||
|
fields: [],
|
||||||
|
global: null,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
const habit = await this.habitCreateUseCase.execute(data)
|
||||||
|
this.setState((state) => {
|
||||||
|
state.habitCreate.state = "success"
|
||||||
|
state.habitsTracker.addHabit(habit)
|
||||||
|
})
|
||||||
|
return "success"
|
||||||
|
} catch (error) {
|
||||||
|
this.setState((state) => {
|
||||||
|
state.habitCreate.state = "error"
|
||||||
|
if (error instanceof ZodError) {
|
||||||
|
state.habitCreate.errors.fields =
|
||||||
|
getErrorsFieldsFromZodError<HabitCreateData>(error)
|
||||||
|
} else {
|
||||||
|
state.habitCreate.errors.global = "unknown"
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return "error"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public async retrieveHabitsTracker(
|
public async retrieveHabitsTracker(
|
||||||
|
@ -1,10 +1,11 @@
|
|||||||
import { zodResolver } from "@hookform/resolvers/zod"
|
import { zodResolver } from "@hookform/resolvers/zod"
|
||||||
import { Controller, useForm } from "react-hook-form"
|
import { Controller, useForm } from "react-hook-form"
|
||||||
|
import { ScrollView, StyleSheet } from "react-native"
|
||||||
import {
|
import {
|
||||||
Appbar,
|
|
||||||
Button,
|
Button,
|
||||||
HelperText,
|
HelperText,
|
||||||
SegmentedButtons,
|
SegmentedButtons,
|
||||||
|
Snackbar,
|
||||||
Text,
|
Text,
|
||||||
TextInput,
|
TextInput,
|
||||||
} from "react-native-paper"
|
} from "react-native-paper"
|
||||||
@ -14,50 +15,26 @@ import ColorPicker, {
|
|||||||
Panel1,
|
Panel1,
|
||||||
Preview,
|
Preview,
|
||||||
} from "reanimated-color-picker"
|
} from "reanimated-color-picker"
|
||||||
|
import { useState } from "react"
|
||||||
|
|
||||||
import type { GoalFrequency, GoalType } from "@/domain/entities/Goal"
|
import type { GoalFrequency, GoalType } from "@/domain/entities/Goal"
|
||||||
import { GOAL_FREQUENCIES, GOAL_TYPES } from "@/domain/entities/Goal"
|
import { GOAL_FREQUENCIES, GOAL_TYPES } from "@/domain/entities/Goal"
|
||||||
import type { HabitCreateData } from "@/domain/entities/Habit"
|
import type { HabitCreateData } from "@/domain/entities/Habit"
|
||||||
import { HabitCreateSchema } from "@/domain/entities/Habit"
|
import { HabitCreateSchema } from "@/domain/entities/Habit"
|
||||||
import type { User } from "@/domain/entities/User"
|
import type { User } from "@/domain/entities/User"
|
||||||
import { capitalize } from "@/presentation/presenters/utils/strings"
|
import { useHabitsTracker } from "../../contexts/HabitsTracker"
|
||||||
|
|
||||||
export interface HabitCreateFormProps {
|
export interface HabitCreateFormProps {
|
||||||
user: User
|
user: User
|
||||||
}
|
}
|
||||||
|
|
||||||
export const HabitCreateForm: React.FC<HabitCreateFormProps> = ({ user }) => {
|
export const HabitCreateForm: React.FC<HabitCreateFormProps> = ({ user }) => {
|
||||||
// const {createHabit, habitPresenter} = useHabitCreate()
|
const { habitCreate, habitsTrackerPresenter } = useHabitsTracker()
|
||||||
|
|
||||||
const onSubmit = async (data: HabitCreateData): Promise<void> => {
|
|
||||||
// await habitPresenter.createHabit(data)
|
|
||||||
console.log(data)
|
|
||||||
}
|
|
||||||
|
|
||||||
const frequenciesIcons: {
|
|
||||||
[key in GoalFrequency]: string
|
|
||||||
} = {
|
|
||||||
daily: "calendar",
|
|
||||||
weekly: "calendar-week",
|
|
||||||
monthly: "calendar-month",
|
|
||||||
}
|
|
||||||
|
|
||||||
const habitTypesTranslations: {
|
|
||||||
[key in GoalType]: { label: string; icon: string }
|
|
||||||
} = {
|
|
||||||
boolean: {
|
|
||||||
label: "Routine",
|
|
||||||
icon: "clock",
|
|
||||||
},
|
|
||||||
numeric: {
|
|
||||||
label: "Target",
|
|
||||||
icon: "target",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
const {
|
const {
|
||||||
control,
|
control,
|
||||||
handleSubmit,
|
handleSubmit,
|
||||||
|
reset,
|
||||||
formState: { errors },
|
formState: { errors },
|
||||||
} = useForm<HabitCreateData>({
|
} = useForm<HabitCreateData>({
|
||||||
mode: "onChange",
|
mode: "onChange",
|
||||||
@ -76,18 +53,57 @@ export const HabitCreateForm: React.FC<HabitCreateFormProps> = ({ user }) => {
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const [isVisibleSnackbar, setIsVisibleSnackbar] = useState(false)
|
||||||
|
|
||||||
|
const onDismissSnackbar = (): void => {
|
||||||
|
setIsVisibleSnackbar(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
const onSubmit = async (data: HabitCreateData): Promise<void> => {
|
||||||
|
await habitsTrackerPresenter.habitCreate(data)
|
||||||
|
setIsVisibleSnackbar(true)
|
||||||
|
reset()
|
||||||
|
}
|
||||||
|
|
||||||
|
const habitFrequenciesTranslations: {
|
||||||
|
[key in GoalFrequency]: { label: string; icon: string }
|
||||||
|
} = {
|
||||||
|
daily: {
|
||||||
|
label: "Daily",
|
||||||
|
icon: "calendar",
|
||||||
|
},
|
||||||
|
weekly: {
|
||||||
|
label: "Weekly",
|
||||||
|
icon: "calendar-week",
|
||||||
|
},
|
||||||
|
monthly: {
|
||||||
|
label: "Monthly",
|
||||||
|
icon: "calendar-month",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
const habitTypesTranslations: {
|
||||||
|
[key in GoalType]: { label: string; icon: string }
|
||||||
|
} = {
|
||||||
|
boolean: {
|
||||||
|
label: "Routine",
|
||||||
|
icon: "clock",
|
||||||
|
},
|
||||||
|
numeric: {
|
||||||
|
label: "Target",
|
||||||
|
icon: "target",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SafeAreaView>
|
<SafeAreaView>
|
||||||
<Appbar.Header>
|
<ScrollView
|
||||||
<Appbar.Content
|
contentContainerStyle={{
|
||||||
title="New Habit"
|
|
||||||
style={{
|
|
||||||
alignItems: "center",
|
|
||||||
justifyContent: "center",
|
justifyContent: "center",
|
||||||
|
alignItems: "center",
|
||||||
|
paddingHorizontal: 20,
|
||||||
}}
|
}}
|
||||||
/>
|
>
|
||||||
</Appbar.Header>
|
|
||||||
|
|
||||||
<Controller
|
<Controller
|
||||||
control={control}
|
control={control}
|
||||||
render={({ field: { onChange, onBlur, value } }) => {
|
render={({ field: { onChange, onBlur, value } }) => {
|
||||||
@ -98,11 +114,16 @@ export const HabitCreateForm: React.FC<HabitCreateFormProps> = ({ user }) => {
|
|||||||
onBlur={onBlur}
|
onBlur={onBlur}
|
||||||
onChangeText={onChange}
|
onChangeText={onChange}
|
||||||
value={value}
|
value={value}
|
||||||
style={[styles.input]}
|
style={[
|
||||||
|
styles.spacing,
|
||||||
|
{
|
||||||
|
width: "90%",
|
||||||
|
},
|
||||||
|
]}
|
||||||
mode="outlined"
|
mode="outlined"
|
||||||
/>
|
/>
|
||||||
{errors.name != null ? (
|
{errors.name != null ? (
|
||||||
<HelperText type="error" visible>
|
<HelperText type="error" visible style={[{ paddingTop: 0 }]}>
|
||||||
{errors.name.type === "too_big"
|
{errors.name.type === "too_big"
|
||||||
? "Name is too long"
|
? "Name is too long"
|
||||||
: "Name is required"}
|
: "Name is required"}
|
||||||
@ -118,10 +139,58 @@ export const HabitCreateForm: React.FC<HabitCreateFormProps> = ({ user }) => {
|
|||||||
control={control}
|
control={control}
|
||||||
render={({ field: { onChange, value } }) => {
|
render={({ field: { onChange, value } }) => {
|
||||||
return (
|
return (
|
||||||
<ColorPicker
|
<>
|
||||||
style={{ width: "70%" }}
|
<Text style={[styles.spacing]}>Habit Frequency</Text>
|
||||||
|
<SegmentedButtons
|
||||||
|
style={[{ width: "90%" }]}
|
||||||
|
onValueChange={onChange}
|
||||||
value={value}
|
value={value}
|
||||||
onChange={(value) => {
|
buttons={GOAL_FREQUENCIES.map((frequency) => {
|
||||||
|
return {
|
||||||
|
label: habitFrequenciesTranslations[frequency].label,
|
||||||
|
value: frequency,
|
||||||
|
icon: habitFrequenciesTranslations[frequency].icon,
|
||||||
|
}
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
name="goal.frequency"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
render={({ field: { onChange, value } }) => {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Text style={[styles.spacing]}>Habit Type</Text>
|
||||||
|
<SegmentedButtons
|
||||||
|
style={[{ width: "90%" }]}
|
||||||
|
onValueChange={onChange}
|
||||||
|
value={value}
|
||||||
|
buttons={GOAL_TYPES.map((type) => {
|
||||||
|
return {
|
||||||
|
label: habitTypesTranslations[type].label,
|
||||||
|
value: type,
|
||||||
|
icon: habitTypesTranslations[type].icon,
|
||||||
|
}
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
name="goal.target.type"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
render={({ field: { onChange, value } }) => {
|
||||||
|
return (
|
||||||
|
<ColorPicker
|
||||||
|
style={[styles.spacing, { width: "90%" }]}
|
||||||
|
value={value}
|
||||||
|
onComplete={(value) => {
|
||||||
onChange(value.hex)
|
onChange(value.hex)
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
@ -143,7 +212,7 @@ export const HabitCreateForm: React.FC<HabitCreateFormProps> = ({ user }) => {
|
|||||||
onBlur={onBlur}
|
onBlur={onBlur}
|
||||||
onChangeText={onChange}
|
onChangeText={onChange}
|
||||||
value={value}
|
value={value}
|
||||||
style={[styles.input]}
|
style={[styles.spacing, { width: "90%" }]}
|
||||||
mode="outlined"
|
mode="outlined"
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
@ -151,66 +220,30 @@ export const HabitCreateForm: React.FC<HabitCreateFormProps> = ({ user }) => {
|
|||||||
name="icon"
|
name="icon"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Controller
|
|
||||||
control={control}
|
|
||||||
render={({ field: { onChange, value } }) => {
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<Text style={{ margin: 8 }}>Habit frequency</Text>
|
|
||||||
<SegmentedButtons
|
|
||||||
onValueChange={onChange}
|
|
||||||
value={value}
|
|
||||||
buttons={GOAL_FREQUENCIES.map((frequency) => {
|
|
||||||
return {
|
|
||||||
label: capitalize(frequency),
|
|
||||||
value: frequency,
|
|
||||||
icon: frequenciesIcons[frequency],
|
|
||||||
}
|
|
||||||
})}
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
}}
|
|
||||||
name="goal.frequency"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Controller
|
|
||||||
control={control}
|
|
||||||
render={({ field: { onChange, value } }) => {
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<Text style={{ margin: 8 }}>Habit type</Text>
|
|
||||||
<SegmentedButtons
|
|
||||||
onValueChange={onChange}
|
|
||||||
value={value}
|
|
||||||
buttons={GOAL_TYPES.map((type) => {
|
|
||||||
return {
|
|
||||||
label: habitTypesTranslations[type].label,
|
|
||||||
value: type,
|
|
||||||
icon: habitTypesTranslations[type].icon,
|
|
||||||
}
|
|
||||||
})}
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
}}
|
|
||||||
name="goal.target.type"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
mode="contained"
|
mode="contained"
|
||||||
onPress={handleSubmit(onSubmit)}
|
onPress={handleSubmit(onSubmit)}
|
||||||
// loading={createHabit.state === "loading"}
|
loading={habitCreate.state === "loading"}
|
||||||
// disabled={createHabit.state === "loading"}
|
disabled={habitCreate.state === "loading"}
|
||||||
|
style={[styles.spacing, { width: "90%" }]}
|
||||||
>
|
>
|
||||||
Create your habit
|
Create your habit! 🚀
|
||||||
</Button>
|
</Button>
|
||||||
|
</ScrollView>
|
||||||
|
|
||||||
|
<Snackbar
|
||||||
|
visible={isVisibleSnackbar}
|
||||||
|
onDismiss={onDismissSnackbar}
|
||||||
|
duration={2_000}
|
||||||
|
>
|
||||||
|
✅ Habit created successfully!
|
||||||
|
</Snackbar>
|
||||||
</SafeAreaView>
|
</SafeAreaView>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const styles = {
|
const styles = StyleSheet.create({
|
||||||
input: {
|
spacing: {
|
||||||
margin: 8,
|
marginVertical: 16,
|
||||||
},
|
},
|
||||||
}
|
})
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
|
import { useRouter } from "expo-router"
|
||||||
|
import { useMemo, useState } from "react"
|
||||||
import { FlatList, View } from "react-native"
|
import { FlatList, View } from "react-native"
|
||||||
import { Button, List, Text } from "react-native-paper"
|
import { Button, List, Text } from "react-native-paper"
|
||||||
import { useMemo, useState } from "react"
|
|
||||||
import { useRouter } from "expo-router"
|
|
||||||
|
|
||||||
import type { GoalFrequency } from "@/domain/entities/Goal"
|
import type { GoalFrequency } from "@/domain/entities/Goal"
|
||||||
import { GOAL_FREQUENCIES } from "@/domain/entities/Goal"
|
import { GOAL_FREQUENCIES } from "@/domain/entities/Goal"
|
||||||
|
Reference in New Issue
Block a user