2024-04-12 23:13:38 +02:00
|
|
|
import type { IconName } from "@fortawesome/free-solid-svg-icons"
|
|
|
|
import { FontAwesomeIcon } from "@fortawesome/react-native-fontawesome"
|
2024-04-04 17:06:42 +02:00
|
|
|
import { zodResolver } from "@hookform/resolvers/zod"
|
2024-04-12 23:13:38 +02:00
|
|
|
import { useState } from "react"
|
2024-04-04 17:06:42 +02:00
|
|
|
import { Controller, useForm } from "react-hook-form"
|
2024-04-12 23:13:38 +02:00
|
|
|
import { ScrollView, StyleSheet, View } from "react-native"
|
2024-04-04 17:26:48 +02:00
|
|
|
import {
|
|
|
|
Button,
|
|
|
|
HelperText,
|
|
|
|
SegmentedButtons,
|
2024-04-05 00:08:40 +02:00
|
|
|
Snackbar,
|
2024-04-04 17:33:51 +02:00
|
|
|
Text,
|
2024-04-04 17:26:48 +02:00
|
|
|
TextInput,
|
|
|
|
} from "react-native-paper"
|
2024-04-04 12:31:56 +02:00
|
|
|
import { SafeAreaView } from "react-native-safe-area-context"
|
2024-04-04 17:06:42 +02:00
|
|
|
import ColorPicker, {
|
|
|
|
HueSlider,
|
|
|
|
Panel1,
|
|
|
|
Preview,
|
|
|
|
} from "reanimated-color-picker"
|
2024-04-04 12:31:56 +02:00
|
|
|
|
2024-04-04 17:33:51 +02:00
|
|
|
import type { GoalFrequency, GoalType } from "@/domain/entities/Goal"
|
|
|
|
import { GOAL_FREQUENCIES, GOAL_TYPES } from "@/domain/entities/Goal"
|
2024-04-04 12:31:56 +02:00
|
|
|
import type { HabitCreateData } from "@/domain/entities/Habit"
|
2024-04-04 17:06:42 +02:00
|
|
|
import { HabitCreateSchema } from "@/domain/entities/Habit"
|
2024-04-04 12:31:56 +02:00
|
|
|
import type { User } from "@/domain/entities/User"
|
2024-05-02 01:08:27 +02:00
|
|
|
import { useHabitsTracker } from "@/presentation/react/contexts/HabitsTracker"
|
|
|
|
import { useBoolean } from "@/presentation/react/hooks/useBoolean"
|
2024-04-12 23:13:38 +02:00
|
|
|
import { IconSelectorModal } from "./IconSelectorModal"
|
2024-04-04 12:31:56 +02:00
|
|
|
|
|
|
|
export interface HabitCreateFormProps {
|
|
|
|
user: User
|
|
|
|
}
|
|
|
|
|
|
|
|
export const HabitCreateForm: React.FC<HabitCreateFormProps> = ({ user }) => {
|
2024-04-05 00:08:40 +02:00
|
|
|
const { habitCreate, habitsTrackerPresenter } = useHabitsTracker()
|
2024-04-04 17:26:48 +02:00
|
|
|
|
2024-04-04 17:06:42 +02:00
|
|
|
const {
|
|
|
|
control,
|
2024-05-02 01:08:27 +02:00
|
|
|
formState: { errors, isValid },
|
2024-04-04 17:06:42 +02:00
|
|
|
handleSubmit,
|
2024-04-05 00:08:40 +02:00
|
|
|
reset,
|
2024-04-13 17:57:58 +02:00
|
|
|
watch,
|
2024-04-04 17:06:42 +02:00
|
|
|
} = useForm<HabitCreateData>({
|
|
|
|
mode: "onChange",
|
|
|
|
resolver: zodResolver(HabitCreateSchema),
|
2024-04-04 12:31:56 +02:00
|
|
|
defaultValues: {
|
|
|
|
userId: user.id,
|
|
|
|
name: "",
|
|
|
|
color: "#006CFF",
|
2024-04-11 15:26:20 +02:00
|
|
|
icon: "circle-question",
|
2024-04-04 12:31:56 +02:00
|
|
|
goal: {
|
|
|
|
frequency: "daily",
|
|
|
|
target: {
|
|
|
|
type: "boolean",
|
|
|
|
},
|
|
|
|
},
|
|
|
|
},
|
|
|
|
})
|
|
|
|
|
2024-04-13 17:57:58 +02:00
|
|
|
const watchGoalType = watch("goal.target.type")
|
|
|
|
|
2024-04-05 00:08:40 +02:00
|
|
|
const [isVisibleSnackbar, setIsVisibleSnackbar] = useState(false)
|
|
|
|
|
2024-04-12 23:13:38 +02:00
|
|
|
const {
|
|
|
|
value: isModalIconSelectorVisible,
|
|
|
|
setTrue: openModalIconSelector,
|
|
|
|
setFalse: closeModalIconSelector,
|
|
|
|
} = useBoolean()
|
|
|
|
|
2024-04-05 00:08:40 +02:00
|
|
|
const onDismissSnackbar = (): void => {
|
|
|
|
setIsVisibleSnackbar(false)
|
|
|
|
}
|
|
|
|
|
|
|
|
const onSubmit = async (data: HabitCreateData): Promise<void> => {
|
|
|
|
await habitsTrackerPresenter.habitCreate(data)
|
|
|
|
setIsVisibleSnackbar(true)
|
2024-04-12 23:13:38 +02:00
|
|
|
closeModalIconSelector()
|
2024-04-05 00:08:40 +02:00
|
|
|
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",
|
|
|
|
},
|
|
|
|
}
|
|
|
|
|
2024-04-04 12:31:56 +02:00
|
|
|
return (
|
|
|
|
<SafeAreaView>
|
2024-04-05 00:08:40 +02:00
|
|
|
<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,
|
|
|
|
{
|
2024-04-13 17:57:58 +02:00
|
|
|
width: "96%",
|
2024-04-05 00:08:40 +02:00
|
|
|
},
|
|
|
|
]}
|
|
|
|
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"
|
|
|
|
/>
|
|
|
|
|
|
|
|
<Controller
|
|
|
|
control={control}
|
|
|
|
render={({ field: { onChange, value } }) => {
|
|
|
|
return (
|
|
|
|
<>
|
|
|
|
<Text style={[styles.spacing]}>Habit Frequency</Text>
|
|
|
|
<SegmentedButtons
|
2024-04-13 17:57:58 +02:00
|
|
|
style={[{ width: "96%" }]}
|
2024-04-05 00:08:40 +02:00
|
|
|
onValueChange={onChange}
|
|
|
|
value={value}
|
|
|
|
buttons={GOAL_FREQUENCIES.map((frequency) => {
|
|
|
|
return {
|
|
|
|
label: habitFrequenciesTranslations[frequency].label,
|
|
|
|
value: frequency,
|
|
|
|
icon: habitFrequenciesTranslations[frequency].icon,
|
|
|
|
}
|
|
|
|
})}
|
|
|
|
/>
|
|
|
|
</>
|
|
|
|
)
|
2024-04-04 12:31:56 +02:00
|
|
|
}}
|
2024-04-05 00:08:40 +02:00
|
|
|
name="goal.frequency"
|
2024-04-04 12:31:56 +02:00
|
|
|
/>
|
2024-04-04 17:06:42 +02:00
|
|
|
|
2024-04-05 00:08:40 +02:00
|
|
|
<Controller
|
|
|
|
control={control}
|
|
|
|
render={({ field: { onChange, value } }) => {
|
|
|
|
return (
|
|
|
|
<>
|
2024-04-11 15:26:20 +02:00
|
|
|
<Text
|
|
|
|
style={[
|
|
|
|
styles.spacing,
|
|
|
|
{ justifyContent: "center", alignContent: "center" },
|
|
|
|
]}
|
|
|
|
>
|
|
|
|
Habit Type
|
|
|
|
</Text>
|
2024-04-05 00:08:40 +02:00
|
|
|
<SegmentedButtons
|
2024-04-13 17:57:58 +02:00
|
|
|
style={[{ width: "96%" }]}
|
2024-04-05 00:08:40 +02:00
|
|
|
onValueChange={onChange}
|
|
|
|
value={value}
|
|
|
|
buttons={GOAL_TYPES.map((type) => {
|
|
|
|
return {
|
|
|
|
label: habitTypesTranslations[type].label,
|
|
|
|
value: type,
|
|
|
|
icon: habitTypesTranslations[type].icon,
|
|
|
|
}
|
|
|
|
})}
|
|
|
|
/>
|
|
|
|
</>
|
|
|
|
)
|
|
|
|
}}
|
|
|
|
name="goal.target.type"
|
|
|
|
/>
|
|
|
|
|
2024-04-13 17:57:58 +02:00
|
|
|
{watchGoalType === "numeric" ? (
|
|
|
|
<View
|
|
|
|
style={{
|
|
|
|
marginTop: 10,
|
|
|
|
flexDirection: "row",
|
|
|
|
gap: 10,
|
|
|
|
width: "96%",
|
|
|
|
}}
|
|
|
|
>
|
|
|
|
<Controller
|
|
|
|
control={control}
|
|
|
|
render={({ field: { onChange, onBlur, value } }) => {
|
|
|
|
return (
|
|
|
|
<TextInput
|
|
|
|
placeholder="Target (e.g: 5 000)"
|
|
|
|
onBlur={onBlur}
|
|
|
|
onChangeText={(text) => {
|
|
|
|
if (text.length <= 0) {
|
|
|
|
onChange("")
|
|
|
|
return
|
|
|
|
}
|
|
|
|
onChange(Number.parseInt(text, 10))
|
|
|
|
}}
|
|
|
|
value={value?.toString()}
|
|
|
|
style={[
|
|
|
|
styles.spacing,
|
|
|
|
{
|
|
|
|
width: "50%",
|
|
|
|
},
|
|
|
|
]}
|
|
|
|
mode="outlined"
|
|
|
|
keyboardType="numeric"
|
|
|
|
/>
|
|
|
|
)
|
|
|
|
}}
|
|
|
|
name="goal.target.value"
|
|
|
|
/>
|
|
|
|
|
|
|
|
<Controller
|
|
|
|
control={control}
|
|
|
|
render={({ field: { onChange, onBlur, value } }) => {
|
|
|
|
return (
|
|
|
|
<TextInput
|
|
|
|
placeholder="Unit (e.g: Steps)"
|
|
|
|
onBlur={onBlur}
|
|
|
|
onChangeText={onChange}
|
|
|
|
value={value}
|
|
|
|
style={[
|
|
|
|
styles.spacing,
|
|
|
|
{
|
|
|
|
width: "50%",
|
|
|
|
},
|
|
|
|
]}
|
|
|
|
mode="outlined"
|
|
|
|
/>
|
|
|
|
)
|
|
|
|
}}
|
|
|
|
name="goal.target.unit"
|
|
|
|
/>
|
|
|
|
</View>
|
|
|
|
) : null}
|
|
|
|
|
2024-04-05 00:08:40 +02:00
|
|
|
<Controller
|
|
|
|
control={control}
|
|
|
|
render={({ field: { onChange, value } }) => {
|
|
|
|
return (
|
|
|
|
<ColorPicker
|
2024-04-13 17:57:58 +02:00
|
|
|
style={[{ marginVertical: 15, width: "96%" }]}
|
2024-04-05 00:08:40 +02:00
|
|
|
value={value}
|
|
|
|
onComplete={(value) => {
|
|
|
|
onChange(value.hex)
|
|
|
|
}}
|
|
|
|
>
|
|
|
|
<Preview hideInitialColor />
|
|
|
|
<Panel1 />
|
|
|
|
<HueSlider />
|
|
|
|
</ColorPicker>
|
|
|
|
)
|
|
|
|
}}
|
|
|
|
name="color"
|
|
|
|
/>
|
|
|
|
|
|
|
|
<Controller
|
|
|
|
control={control}
|
2024-04-12 10:29:23 +02:00
|
|
|
render={({ field: { onChange, value } }) => {
|
2024-04-05 00:08:40 +02:00
|
|
|
return (
|
2024-04-12 23:13:38 +02:00
|
|
|
<View
|
|
|
|
style={{
|
|
|
|
justifyContent: "center",
|
|
|
|
alignItems: "center",
|
|
|
|
flexDirection: "row",
|
|
|
|
gap: 20,
|
2024-04-13 17:57:58 +02:00
|
|
|
marginVertical: 5,
|
2024-04-12 23:13:38 +02:00
|
|
|
}}
|
|
|
|
>
|
|
|
|
<FontAwesomeIcon size={36} icon={value as IconName} />
|
|
|
|
<Button mode="contained" onPress={openModalIconSelector}>
|
|
|
|
Choose an icon
|
|
|
|
</Button>
|
|
|
|
|
|
|
|
<IconSelectorModal
|
|
|
|
key={isModalIconSelectorVisible ? "visible" : "hidden"}
|
|
|
|
isVisible={isModalIconSelectorVisible}
|
|
|
|
selectedIcon={value}
|
|
|
|
handleCloseModal={closeModalIconSelector}
|
|
|
|
onIconSelect={onChange}
|
|
|
|
/>
|
|
|
|
</View>
|
2024-04-05 00:08:40 +02:00
|
|
|
)
|
|
|
|
}}
|
|
|
|
name="icon"
|
|
|
|
/>
|
|
|
|
|
|
|
|
<Button
|
|
|
|
mode="contained"
|
|
|
|
onPress={handleSubmit(onSubmit)}
|
|
|
|
loading={habitCreate.state === "loading"}
|
2024-04-13 17:57:58 +02:00
|
|
|
disabled={habitCreate.state === "loading" || !isValid}
|
|
|
|
style={[{ width: "100%", marginVertical: 15 }]}
|
2024-04-05 00:08:40 +02:00
|
|
|
>
|
|
|
|
Create your habit! 🚀
|
|
|
|
</Button>
|
|
|
|
</ScrollView>
|
|
|
|
|
|
|
|
<Snackbar
|
|
|
|
visible={isVisibleSnackbar}
|
|
|
|
onDismiss={onDismissSnackbar}
|
|
|
|
duration={2_000}
|
2024-04-04 17:06:42 +02:00
|
|
|
>
|
2024-04-05 00:08:40 +02:00
|
|
|
✅ Habit created successfully!
|
|
|
|
</Snackbar>
|
2024-04-04 12:31:56 +02:00
|
|
|
</SafeAreaView>
|
|
|
|
)
|
|
|
|
}
|
2024-04-04 17:06:42 +02:00
|
|
|
|
2024-04-05 00:08:40 +02:00
|
|
|
const styles = StyleSheet.create({
|
|
|
|
spacing: {
|
2024-04-13 17:57:58 +02:00
|
|
|
marginVertical: 10,
|
2024-04-04 17:06:42 +02:00
|
|
|
},
|
2024-04-05 00:08:40 +02:00
|
|
|
})
|