1
1
mirror of https://github.com/theoludwig/p61-project.git synced 2024-07-17 07:00:12 +02:00

feat: habit create use case

This commit is contained in:
Théo LUDWIG 2024-04-05 00:08:40 +02:00
parent a2d187a27a
commit e4fcb1894c
Signed by: theoludwig
GPG Key ID: ADFE5A563D718F3B
17 changed files with 648 additions and 506 deletions

View File

@ -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 = () => {
}, },
}} }}
> >
<StackLayout /> <GestureHandlerRootView style={{ flex: 1 }}>
<StackLayout />
</GestureHandlerRootView>
<StatusBar style="dark" /> <StatusBar style="dark" />
</PaperProvider> </PaperProvider>

View File

@ -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"

View File

@ -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"
} }

View File

@ -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

View File

@ -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
} }

View File

@ -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: [],
}),
)
}
} }

View 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>
}

View 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
}
}

View File

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

View 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
}
}

View File

@ -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);

View File

@ -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

File diff suppressed because it is too large Load Diff

View File

@ -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"
} }
} }

View File

@ -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(

View File

@ -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,141 +53,197 @@ 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" justifyContent: "center",
style={{ alignItems: "center",
alignItems: "center", paddingHorizontal: 20,
justifyContent: "center", }}
>
<Controller
control={control}
render={({ field: { onChange, onBlur, value } }) => {
return (
<>
<TextInput
placeholder="Name"
onBlur={onBlur}
onChangeText={onChange}
value={value}
style={[
styles.spacing,
{
width: "90%",
},
]}
mode="outlined"
/>
{errors.name != null ? (
<HelperText type="error" visible style={[{ paddingTop: 0 }]}>
{errors.name.type === "too_big"
? "Name is too long"
: "Name is required"}
</HelperText>
) : null}
</>
)
}} }}
name="name"
/> />
</Appbar.Header>
<Controller <Controller
control={control} control={control}
render={({ field: { onChange, onBlur, value } }) => { render={({ field: { onChange, value } }) => {
return ( return (
<> <>
<Text style={[styles.spacing]}>Habit Frequency</Text>
<SegmentedButtons
style={[{ width: "90%" }]}
onValueChange={onChange}
value={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)
}}
>
<Preview hideInitialColor />
<Panel1 />
<HueSlider />
</ColorPicker>
)
}}
name="color"
/>
<Controller
control={control}
render={({ field: { onChange, onBlur, value } }) => {
return (
<TextInput <TextInput
placeholder="Name" placeholder="Icon"
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 ? ( )
<HelperText type="error" visible> }}
{errors.name.type === "too_big" name="icon"
? "Name is too long" />
: "Name is required"}
</HelperText>
) : null}
</>
)
}}
name="name"
/>
<Controller <Button
control={control} mode="contained"
render={({ field: { onChange, value } }) => { onPress={handleSubmit(onSubmit)}
return ( loading={habitCreate.state === "loading"}
<ColorPicker disabled={habitCreate.state === "loading"}
style={{ width: "70%" }} style={[styles.spacing, { width: "90%" }]}
value={value} >
onChange={(value) => { Create your habit! 🚀
onChange(value.hex) </Button>
}} </ScrollView>
>
<Preview hideInitialColor />
<Panel1 />
<HueSlider />
</ColorPicker>
)
}}
name="color"
/>
<Controller <Snackbar
control={control} visible={isVisibleSnackbar}
render={({ field: { onChange, onBlur, value } }) => { onDismiss={onDismissSnackbar}
return ( duration={2_000}
<TextInput
placeholder="Icon"
onBlur={onBlur}
onChangeText={onChange}
value={value}
style={[styles.input]}
mode="outlined"
/>
)
}}
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
mode="contained"
onPress={handleSubmit(onSubmit)}
// loading={createHabit.state === "loading"}
// disabled={createHabit.state === "loading"}
> >
Create your habit Habit created successfully!
</Button> </Snackbar>
</SafeAreaView> </SafeAreaView>
) )
} }
const styles = { const styles = StyleSheet.create({
input: { spacing: {
margin: 8, marginVertical: 16,
}, },
} })

View File

@ -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"