feat: habit create use case
This commit is contained in:
parent
a2d187a27a
commit
e4fcb1894c
@ -6,6 +6,7 @@ import {
|
||||
} from "react-native-paper"
|
||||
import { StatusBar } from "expo-status-bar"
|
||||
import { useEffect } from "react"
|
||||
import { GestureHandlerRootView } from "react-native-gesture-handler"
|
||||
|
||||
import { HabitsTrackerProvider } from "@/presentation/react/contexts/HabitsTracker"
|
||||
import {
|
||||
@ -61,7 +62,9 @@ const RootLayout: React.FC = () => {
|
||||
},
|
||||
}}
|
||||
>
|
||||
<StackLayout />
|
||||
<GestureHandlerRootView style={{ flex: 1 }}>
|
||||
<StackLayout />
|
||||
</GestureHandlerRootView>
|
||||
|
||||
<StatusBar style="dark" />
|
||||
</PaperProvider>
|
||||
|
@ -30,7 +30,8 @@ const HabitsPage: React.FC = () => {
|
||||
) : retrieveHabitsTracker.state === "error" ? (
|
||||
<>
|
||||
<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>
|
||||
<Button
|
||||
mode="contained"
|
||||
|
@ -12,8 +12,8 @@ export const GOAL_FREQUENCIES = GOAL_FREQUENCIES_ZOD.map((frequency) => {
|
||||
export type GoalFrequency = (typeof GOAL_FREQUENCIES)[number]
|
||||
|
||||
export const GOAL_TYPES_ZOD = [
|
||||
z.literal("numeric"),
|
||||
z.literal("boolean"),
|
||||
z.literal("numeric"),
|
||||
] as const
|
||||
export const goalTypeZod = z.union(GOAL_TYPES_ZOD)
|
||||
export const GOAL_TYPES = GOAL_TYPES_ZOD.map((type) => {
|
||||
@ -51,6 +51,16 @@ export abstract class Goal implements GoalBase {
|
||||
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 {
|
||||
return goal.type === "numeric"
|
||||
}
|
||||
|
@ -15,15 +15,15 @@ export const HabitCreateSchema = HabitSchema.extend({
|
||||
}).omit({ id: true })
|
||||
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
|
||||
startDate: Date
|
||||
endDate?: Date
|
||||
}
|
||||
|
||||
export interface HabitJSON extends HabitDataBase {
|
||||
export interface HabitJSON extends HabitBase {
|
||||
goal: GoalBaseJSON
|
||||
startDate: string
|
||||
endDate?: string
|
||||
|
@ -3,16 +3,16 @@ import type { Habit } from "./Habit"
|
||||
import type { EntityData } from "./_Entity"
|
||||
import { Entity } from "./_Entity"
|
||||
|
||||
interface HabitProgressDataBase extends EntityData {
|
||||
interface HabitProgressBase extends EntityData {
|
||||
habitId: Habit["id"]
|
||||
}
|
||||
|
||||
export interface HabitProgressData extends HabitProgressDataBase {
|
||||
export interface HabitProgressData extends HabitProgressBase {
|
||||
goalProgress: GoalProgress
|
||||
date: Date
|
||||
}
|
||||
|
||||
export interface HabitProgressJSON extends HabitProgressDataBase {
|
||||
export interface HabitProgressJSON extends HabitProgressBase {
|
||||
goalProgress: GoalProgressBase
|
||||
date: string
|
||||
}
|
||||
|
@ -1,5 +1,6 @@
|
||||
import type { GoalFrequency } from "./Goal"
|
||||
import type { HabitHistory } from "./HabitHistory"
|
||||
import type { Habit } from "./Habit"
|
||||
import { HabitHistory } from "./HabitHistory"
|
||||
|
||||
export interface HabitsTrackerData {
|
||||
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 { supabaseClient } from "./supabase/supabase"
|
||||
import { AuthenticationPresenter } from "@/presentation/presenters/Authentication"
|
||||
import { HabitCreateSupabaseRepository } from "./supabase/repositories/HabitCreate"
|
||||
import { HabitCreateUseCase } from "@/domain/use-cases/HabitCreate"
|
||||
|
||||
/**
|
||||
* Repositories
|
||||
@ -20,6 +22,9 @@ const getHabitProgressesRepository =
|
||||
const getHabitsByUserIdRepository = new GetHabitsByUserIdSupabaseRepository({
|
||||
supabaseClient,
|
||||
})
|
||||
const habitCreateRepository = new HabitCreateSupabaseRepository({
|
||||
supabaseClient,
|
||||
})
|
||||
|
||||
/**
|
||||
* Use Cases
|
||||
@ -27,6 +32,9 @@ const getHabitsByUserIdRepository = new GetHabitsByUserIdSupabaseRepository({
|
||||
const authenticationUseCase = new AuthenticationUseCase({
|
||||
authenticationRepository,
|
||||
})
|
||||
const habitCreateUseCase = new HabitCreateUseCase({
|
||||
habitCreateRepository,
|
||||
})
|
||||
const retrieveHabitsTrackerUseCase = new RetrieveHabitsTrackerUseCase({
|
||||
getHabitProgressHistoryRepository: getHabitProgressesRepository,
|
||||
getHabitsByUserIdRepository,
|
||||
@ -40,4 +48,5 @@ export const authenticationPresenter = new AuthenticationPresenter({
|
||||
})
|
||||
export const habitsTrackerPresenter = new HabitsTrackerPresenter({
|
||||
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()),
|
||||
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"
|
||||
|
||||
const SUPABASE_URL = process.env["EXPO_PUBLIC_SUPABASE_URL"] ?? ""
|
||||
const SUPABASE_ANON_KEY = process.env["EXPO_PUBLIC_SUPABASE_ANON_KEY"] ?? ""
|
||||
const SUPABASE_URL =
|
||||
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>(
|
||||
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",
|
||||
"@react-native-async-storage/async-storage": "1.21.0",
|
||||
"@react-navigation/native": "6.1.16",
|
||||
"@supabase/supabase-js": "2.39.8",
|
||||
"@supabase/supabase-js": "2.42.0",
|
||||
"expo": "50.0.14",
|
||||
"expo-font": "11.10.3",
|
||||
"expo-linking": "6.2.2",
|
||||
@ -35,13 +35,13 @@
|
||||
"immer": "10.0.4",
|
||||
"react": "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-calendars": "1.1304.1",
|
||||
"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-reanimated": "~3.6.2",
|
||||
"react-native-reanimated": "3.6.3",
|
||||
"react-native-safe-area-context": "4.8.2",
|
||||
"react-native-screens": "3.29.0",
|
||||
"react-native-url-polyfill": "2.0.0",
|
||||
@ -51,18 +51,18 @@
|
||||
"zod": "3.22.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "7.24.3",
|
||||
"@babel/core": "7.24.4",
|
||||
"@commitlint/cli": "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",
|
||||
"@tsconfig/strictest": "2.0.3",
|
||||
"@tsconfig/strictest": "2.0.5",
|
||||
"@types/jest": "29.5.12",
|
||||
"@types/node": "20.11.30",
|
||||
"@types/react": "18.2.69",
|
||||
"@types/node": "20.12.4",
|
||||
"@types/react": "18.2.74",
|
||||
"@types/react-test-renderer": "18.0.7",
|
||||
"@typescript-eslint/eslint-plugin": "7.3.1",
|
||||
"@typescript-eslint/parser": "7.3.1",
|
||||
"@typescript-eslint/eslint-plugin": "7.5.0",
|
||||
"@typescript-eslint/parser": "7.5.0",
|
||||
"eslint": "8.57.0",
|
||||
"eslint-config-conventions": "14.1.0",
|
||||
"eslint-config-prettier": "9.1.0",
|
||||
@ -79,7 +79,7 @@
|
||||
"jest-junit": "16.0.0",
|
||||
"lint-staged": "15.2.2",
|
||||
"react-test-renderer": "18.2.0",
|
||||
"supabase": "1.150.0",
|
||||
"typescript": "5.4.3"
|
||||
"supabase": "1.153.4",
|
||||
"typescript": "5.4.4"
|
||||
}
|
||||
}
|
||||
|
@ -1,10 +1,15 @@
|
||||
import { ZodError } from "zod"
|
||||
|
||||
import { HabitsTracker } from "@/domain/entities/HabitsTracker"
|
||||
import type { FetchState } from "./_Presenter"
|
||||
import type { ErrorGlobal, FetchState } from "./_Presenter"
|
||||
import { Presenter } from "./_Presenter"
|
||||
import type {
|
||||
RetrieveHabitsTrackerUseCase,
|
||||
RetrieveHabitsTrackerUseCaseOptions,
|
||||
} 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 {
|
||||
habitsTracker: HabitsTracker
|
||||
@ -12,10 +17,19 @@ export interface HabitsTrackerPresenterState {
|
||||
retrieveHabitsTracker: {
|
||||
state: FetchState
|
||||
}
|
||||
|
||||
habitCreate: {
|
||||
state: FetchState
|
||||
errors: {
|
||||
fields: Array<keyof HabitCreateData>
|
||||
global: ErrorGlobal
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export interface HabitsTrackerPresenterOptions {
|
||||
retrieveHabitsTrackerUseCase: RetrieveHabitsTrackerUseCase
|
||||
habitCreateUseCase: HabitCreateUseCase
|
||||
}
|
||||
|
||||
export class HabitsTrackerPresenter
|
||||
@ -23,12 +37,53 @@ export class HabitsTrackerPresenter
|
||||
implements HabitsTrackerPresenterOptions
|
||||
{
|
||||
public retrieveHabitsTrackerUseCase: RetrieveHabitsTrackerUseCase
|
||||
public habitCreateUseCase: HabitCreateUseCase
|
||||
|
||||
public constructor(options: HabitsTrackerPresenterOptions) {
|
||||
const { retrieveHabitsTrackerUseCase } = options
|
||||
const { retrieveHabitsTrackerUseCase, habitCreateUseCase } = options
|
||||
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.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(
|
||||
|
@ -1,10 +1,11 @@
|
||||
import { zodResolver } from "@hookform/resolvers/zod"
|
||||
import { Controller, useForm } from "react-hook-form"
|
||||
import { ScrollView, StyleSheet } from "react-native"
|
||||
import {
|
||||
Appbar,
|
||||
Button,
|
||||
HelperText,
|
||||
SegmentedButtons,
|
||||
Snackbar,
|
||||
Text,
|
||||
TextInput,
|
||||
} from "react-native-paper"
|
||||
@ -14,50 +15,26 @@ import ColorPicker, {
|
||||
Panel1,
|
||||
Preview,
|
||||
} from "reanimated-color-picker"
|
||||
import { useState } from "react"
|
||||
|
||||
import type { GoalFrequency, GoalType } from "@/domain/entities/Goal"
|
||||
import { GOAL_FREQUENCIES, GOAL_TYPES } from "@/domain/entities/Goal"
|
||||
import type { HabitCreateData } from "@/domain/entities/Habit"
|
||||
import { HabitCreateSchema } from "@/domain/entities/Habit"
|
||||
import type { User } from "@/domain/entities/User"
|
||||
import { capitalize } from "@/presentation/presenters/utils/strings"
|
||||
import { useHabitsTracker } from "../../contexts/HabitsTracker"
|
||||
|
||||
export interface HabitCreateFormProps {
|
||||
user: User
|
||||
}
|
||||
|
||||
export const HabitCreateForm: React.FC<HabitCreateFormProps> = ({ user }) => {
|
||||
// const {createHabit, habitPresenter} = useHabitCreate()
|
||||
|
||||
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 { habitCreate, habitsTrackerPresenter } = useHabitsTracker()
|
||||
|
||||
const {
|
||||
control,
|
||||
handleSubmit,
|
||||
reset,
|
||||
formState: { errors },
|
||||
} = useForm<HabitCreateData>({
|
||||
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 (
|
||||
<SafeAreaView>
|
||||
<Appbar.Header>
|
||||
<Appbar.Content
|
||||
title="New Habit"
|
||||
style={{
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
<ScrollView
|
||||
contentContainerStyle={{
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
paddingHorizontal: 20,
|
||||
}}
|
||||
>
|
||||
<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
|
||||
control={control}
|
||||
render={({ field: { onChange, onBlur, value } }) => {
|
||||
return (
|
||||
<>
|
||||
<Controller
|
||||
control={control}
|
||||
render={({ field: { onChange, value } }) => {
|
||||
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
|
||||
placeholder="Name"
|
||||
placeholder="Icon"
|
||||
onBlur={onBlur}
|
||||
onChangeText={onChange}
|
||||
value={value}
|
||||
style={[styles.input]}
|
||||
style={[styles.spacing, { width: "90%" }]}
|
||||
mode="outlined"
|
||||
/>
|
||||
{errors.name != null ? (
|
||||
<HelperText type="error" visible>
|
||||
{errors.name.type === "too_big"
|
||||
? "Name is too long"
|
||||
: "Name is required"}
|
||||
</HelperText>
|
||||
) : null}
|
||||
</>
|
||||
)
|
||||
}}
|
||||
name="name"
|
||||
/>
|
||||
)
|
||||
}}
|
||||
name="icon"
|
||||
/>
|
||||
|
||||
<Controller
|
||||
control={control}
|
||||
render={({ field: { onChange, value } }) => {
|
||||
return (
|
||||
<ColorPicker
|
||||
style={{ width: "70%" }}
|
||||
value={value}
|
||||
onChange={(value) => {
|
||||
onChange(value.hex)
|
||||
}}
|
||||
>
|
||||
<Preview hideInitialColor />
|
||||
<Panel1 />
|
||||
<HueSlider />
|
||||
</ColorPicker>
|
||||
)
|
||||
}}
|
||||
name="color"
|
||||
/>
|
||||
<Button
|
||||
mode="contained"
|
||||
onPress={handleSubmit(onSubmit)}
|
||||
loading={habitCreate.state === "loading"}
|
||||
disabled={habitCreate.state === "loading"}
|
||||
style={[styles.spacing, { width: "90%" }]}
|
||||
>
|
||||
Create your habit! 🚀
|
||||
</Button>
|
||||
</ScrollView>
|
||||
|
||||
<Controller
|
||||
control={control}
|
||||
render={({ field: { onChange, onBlur, value } }) => {
|
||||
return (
|
||||
<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"}
|
||||
<Snackbar
|
||||
visible={isVisibleSnackbar}
|
||||
onDismiss={onDismissSnackbar}
|
||||
duration={2_000}
|
||||
>
|
||||
Create your habit
|
||||
</Button>
|
||||
✅ Habit created successfully!
|
||||
</Snackbar>
|
||||
</SafeAreaView>
|
||||
)
|
||||
}
|
||||
|
||||
const styles = {
|
||||
input: {
|
||||
margin: 8,
|
||||
const styles = StyleSheet.create({
|
||||
spacing: {
|
||||
marginVertical: 16,
|
||||
},
|
||||
}
|
||||
})
|
||||
|
@ -1,7 +1,7 @@
|
||||
import { useRouter } from "expo-router"
|
||||
import { useMemo, useState } from "react"
|
||||
import { FlatList, View } from "react-native"
|
||||
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 { GOAL_FREQUENCIES } from "@/domain/entities/Goal"
|
||||
|
Reference in New Issue
Block a user