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

refactor: habits history component

This commit is contained in:
Théo LUDWIG 2024-03-24 23:41:23 +01:00
parent 39ebe3a152
commit 1c648972d5
Signed by: theoludwig
GPG Key ID: ADFE5A563D718F3B
18 changed files with 254 additions and 164 deletions

View File

@ -36,6 +36,12 @@ const TabLayout: React.FC = () => {
}, },
}} }}
/> />
<Tabs.Screen
name="habits/[habitId]"
options={{
href: null,
}}
/>
<Tabs.Screen <Tabs.Screen
name="habits/history" name="habits/history"
options={{ options={{

View File

@ -0,0 +1,7 @@
import { Slot } from "expo-router"
const HabitLayout: React.FC = () => {
return <Slot />
}
export default HabitLayout

View File

@ -0,0 +1,22 @@
import { useLocalSearchParams } from "expo-router"
import { Text } from "react-native-paper"
import { SafeAreaView } from "react-native-safe-area-context"
const HabitPage: React.FC = () => {
const { habitId } = useLocalSearchParams()
return (
<SafeAreaView
style={[
{
flex: 1,
alignItems: "center",
},
]}
>
<Text>Habit Page {habitId}</Text>
</SafeAreaView>
)
}
export default HabitPage

View File

@ -1,5 +1,4 @@
import { useState } from "react" import { useState } from "react"
import { StyleSheet } from "react-native"
import { Calendar } from "react-native-calendars" import { Calendar } from "react-native-calendars"
import { SafeAreaView } from "react-native-safe-area-context" import { SafeAreaView } from "react-native-safe-area-context"
@ -7,7 +6,15 @@ const HistoryPage: React.FC = () => {
const [selected, setSelected] = useState("") const [selected, setSelected] = useState("")
return ( return (
<SafeAreaView style={styles.container}> <SafeAreaView
style={[
{
flex: 1,
alignItems: "center",
justifyContent: "center",
},
]}
>
<Calendar <Calendar
onDayPress={(day) => { onDayPress={(day) => {
setSelected(day.dateString) setSelected(day.dateString)
@ -37,12 +44,4 @@ const HistoryPage: React.FC = () => {
) )
} }
const styles = StyleSheet.create({
container: {
flex: 1,
alignItems: "center",
justifyContent: "center",
},
})
export default HistoryPage export default HistoryPage

View File

@ -1,79 +1,23 @@
import FontAwesome6 from "@expo/vector-icons/FontAwesome6"
import { FlatList, StyleSheet } from "react-native"
import { List } from "react-native-paper"
import { SafeAreaView } from "react-native-safe-area-context" import { SafeAreaView } from "react-native-safe-area-context"
import { HabitsHistory } from "@/presentation/react/components/HabitsHistory/HabitsHistory"
import { useHabitsTracker } from "@/presentation/react/contexts/HabitsTracker" import { useHabitsTracker } from "@/presentation/react/contexts/HabitsTracker"
import { colorsPresenter } from "@/presentation/presenters/utils/ColorsPresenter"
const HabitsPage: React.FC = () => { const HabitsPage: React.FC = () => {
const { habitsTracker } = useHabitsTracker() const { habitsTracker } = useHabitsTracker()
return ( return (
<SafeAreaView style={[styles.container]}> <SafeAreaView
<List.Section style={[styles.habitsList]}>
<FlatList
data={habitsTracker.habitsHistory}
renderItem={({ item }) => {
const { habit } = item
return (
<List.Item
title={habit.name}
style={[ style={[
styles.habitItem,
{ {
backgroundColor: colorsPresenter.hexToRgbA( flex: 1,
habit.color, alignItems: "center",
0.4,
),
}, },
]} ]}
contentStyle={[ >
{ <HabitsHistory habitsHistory={habitsTracker.habitsHistory} />
paddingLeft: 12,
},
]}
titleStyle={[
{
fontSize: 18,
},
]}
left={() => {
return (
<FontAwesome6
size={24}
name={habit.icon}
style={[styles.habitItemIcon]}
/>
)
}}
/>
)
}}
/>
</List.Section>
</SafeAreaView> </SafeAreaView>
) )
} }
const styles = StyleSheet.create({
container: {
flex: 1,
alignItems: "center",
},
habitsList: {
width: "90%",
},
habitItem: {
paddingVertical: 20,
paddingHorizontal: 10,
marginVertical: 10,
borderRadius: 10,
},
habitItemIcon: {
width: 30,
},
})
export default HabitsPage export default HabitsPage

View File

@ -1,21 +1,20 @@
import { StyleSheet } from "react-native"
import { Text } from "react-native-paper" import { Text } from "react-native-paper"
import { SafeAreaView } from "react-native-safe-area-context" import { SafeAreaView } from "react-native-safe-area-context"
const NewHabitPage: React.FC = () => { const NewHabitPage: React.FC = () => {
return ( return (
<SafeAreaView style={styles.container}> <SafeAreaView
style={[
{
flex: 1,
alignItems: "center",
justifyContent: "center",
},
]}
>
<Text>New Habit</Text> <Text>New Habit</Text>
</SafeAreaView> </SafeAreaView>
) )
} }
const styles = StyleSheet.create({
container: {
flex: 1,
alignItems: "center",
justifyContent: "center",
},
})
export default NewHabitPage export default NewHabitPage

View File

@ -1,4 +1,4 @@
import { StyleSheet, Text } from "react-native" import { Text } from "react-native"
import { Button } from "react-native-paper" import { Button } from "react-native-paper"
import { SafeAreaView } from "react-native-safe-area-context" import { SafeAreaView } from "react-native-safe-area-context"
@ -12,7 +12,15 @@ const SettingsPage: React.FC = () => {
} }
return ( return (
<SafeAreaView style={styles.container}> <SafeAreaView
style={[
{
flex: 1,
alignItems: "center",
justifyContent: "center",
},
]}
>
<Text>Settings</Text> <Text>Settings</Text>
<Button <Button
@ -27,12 +35,4 @@ const SettingsPage: React.FC = () => {
) )
} }
const styles = StyleSheet.create({
container: {
flex: 1,
alignItems: "center",
justifyContent: "center",
},
})
export default SettingsPage export default SettingsPage

View File

@ -21,7 +21,7 @@ const LoginPage: React.FC = () => {
} }
return ( return (
<SafeAreaView style={styles.container}> <SafeAreaView style={[styles.container]}>
<Controller <Controller
control={control} control={control}
render={({ field: { onChange, onBlur, value } }) => { render={({ field: { onChange, onBlur, value } }) => {
@ -31,7 +31,7 @@ const LoginPage: React.FC = () => {
onBlur={onBlur} onBlur={onBlur}
onChangeText={onChange} onChangeText={onChange}
value={value} value={value}
style={styles.input} style={[styles.input]}
mode="outlined" mode="outlined"
/> />
) )
@ -48,7 +48,7 @@ const LoginPage: React.FC = () => {
onBlur={onBlur} onBlur={onBlur}
onChangeText={onChange} onChangeText={onChange}
value={value} value={value}
style={styles.input} style={[styles.input]}
mode="outlined" mode="outlined"
secureTextEntry secureTextEntry
/> />
@ -60,7 +60,7 @@ const LoginPage: React.FC = () => {
<HelperText <HelperText
type="error" type="error"
visible={login.state === "error"} visible={login.state === "error"}
style={styles.helperText} style={[styles.helperText]}
> >
Invalid credentials. Invalid credentials.
</HelperText> </HelperText>

View File

@ -24,13 +24,13 @@ const RegisterPage: React.FC = () => {
const helperMessage = useMemo(() => { const helperMessage = useMemo(() => {
if (register.state === "error") { if (register.state === "error") {
if (register.errorsFields.includes("displayName")) { if (register.errors.fields.includes("displayName")) {
return "Display Name is required." return "Display Name is required."
} }
if (register.errorsFields.includes("email")) { if (register.errors.fields.includes("email")) {
return "Invalid email." return "Invalid email."
} }
if (register.errorsFields.includes("password")) { if (register.errors.fields.includes("password")) {
return "Password must be at least 6 characters." return "Password must be at least 6 characters."
} }
return "Invalid credentials." return "Invalid credentials."
@ -41,10 +41,10 @@ const RegisterPage: React.FC = () => {
// } // }
return "" return ""
}, [register.errorsFields, register.state]) }, [register.errors.fields, register.state])
return ( return (
<SafeAreaView style={styles.container}> <SafeAreaView style={[styles.container]}>
<Controller <Controller
control={control} control={control}
render={({ field: { onChange, onBlur, value } }) => { render={({ field: { onChange, onBlur, value } }) => {
@ -54,7 +54,7 @@ const RegisterPage: React.FC = () => {
onBlur={onBlur} onBlur={onBlur}
onChangeText={onChange} onChangeText={onChange}
value={value} value={value}
style={styles.input} style={[styles.input]}
mode="outlined" mode="outlined"
/> />
) )
@ -71,7 +71,7 @@ const RegisterPage: React.FC = () => {
onBlur={onBlur} onBlur={onBlur}
onChangeText={onChange} onChangeText={onChange}
value={value} value={value}
style={styles.input} style={[styles.input]}
mode="outlined" mode="outlined"
/> />
) )
@ -88,7 +88,7 @@ const RegisterPage: React.FC = () => {
onBlur={onBlur} onBlur={onBlur}
onChangeText={onChange} onChangeText={onChange}
value={value} value={value}
style={styles.input} style={[styles.input]}
mode="outlined" mode="outlined"
secureTextEntry secureTextEntry
/> />
@ -100,7 +100,7 @@ const RegisterPage: React.FC = () => {
<HelperText <HelperText
type={register.state === "error" ? "error" : "info"} type={register.state === "error" ? "error" : "info"}
visible={register.state === "error" || register.state === "success"} visible={register.state === "error" || register.state === "success"}
style={styles.helperText} style={[styles.helperText]}
> >
{helperMessage} {helperMessage}
</HelperText> </HelperText>

View File

@ -1,8 +1,5 @@
import { import type { User } from "../entities/User"
UserRegisterSchema, import { UserLoginSchema, UserRegisterSchema } from "../entities/User"
type User,
UserLoginSchema,
} from "../entities/User"
import type { AuthenticationRepository } from "../repositories/Authentication" import type { AuthenticationRepository } from "../repositories/Authentication"
export interface AuthenticationUseCaseDependencyOptions { export interface AuthenticationUseCaseDependencyOptions {
@ -42,16 +39,16 @@ export class AuthenticationUseCase
return await this.authenticationRepository.login(userData) return await this.authenticationRepository.login(userData)
} }
public async logout(): Promise<void> { public logout: AuthenticationRepository["logout"] = async () => {
return await this.authenticationRepository.logout() return await this.authenticationRepository.logout()
} }
public getUser: AuthenticationRepository["getUser"] = async (...args) => { public getUser: AuthenticationRepository["getUser"] = async () => {
return await this.authenticationRepository.getUser(...args) return await this.authenticationRepository.getUser()
} }
public onUserStateChange: AuthenticationRepository["onUserStateChange"] = public onUserStateChange: AuthenticationRepository["onUserStateChange"] =
async (...args) => { async (callback) => {
return this.authenticationRepository.onUserStateChange(...args) return this.authenticationRepository.onUserStateChange(callback)
} }
} }

View File

@ -8,7 +8,7 @@ import type {
import type { AuthenticationUseCase } from "@/domain/use-cases/Authentication" import type { AuthenticationUseCase } from "@/domain/use-cases/Authentication"
import type { ErrorGlobal, FetchState } from "./_Presenter" import type { ErrorGlobal, FetchState } from "./_Presenter"
import { Presenter } from "./_Presenter" import { Presenter } from "./_Presenter"
import { zodPresenter } from "./utils/ZodPresenter" import { getErrorsFieldsFromZodError } from "./utils/zod"
export interface AuthenticationPresenterState { export interface AuthenticationPresenterState {
user: User | null user: User | null
@ -20,14 +20,18 @@ export interface AuthenticationPresenterState {
register: { register: {
state: FetchState state: FetchState
errorsFields: Array<keyof UserRegisterData> errors: {
errorGlobal: ErrorGlobal fields: Array<keyof UserRegisterData>
global: ErrorGlobal
}
} }
login: { login: {
state: FetchState state: FetchState
errorsFields: Array<keyof UserLoginData> errors: {
errorGlobal: ErrorGlobal fields: Array<keyof UserLoginData>
global: ErrorGlobal
}
} }
logout: { logout: {
@ -52,13 +56,17 @@ export class AuthenticationPresenter
hasLoaded: true, hasLoaded: true,
register: { register: {
state: "idle", state: "idle",
errorsFields: [], errors: {
errorGlobal: null, fields: [],
global: null,
},
}, },
login: { login: {
state: "idle", state: "idle",
errorsFields: [], errors: {
errorGlobal: null, fields: [],
global: null,
},
}, },
logout: { logout: {
state: "idle", state: "idle",
@ -71,8 +79,10 @@ export class AuthenticationPresenter
try { try {
this.setState((state) => { this.setState((state) => {
state.register.state = "loading" state.register.state = "loading"
state.register.errorsFields = [] state.register.errors = {
state.register.errorGlobal = null fields: [],
global: null,
}
}) })
const user = await this.authenticationUseCase.register(data) const user = await this.authenticationUseCase.register(data)
this.setState((state) => { this.setState((state) => {
@ -83,10 +93,10 @@ export class AuthenticationPresenter
this.setState((state) => { this.setState((state) => {
state.register.state = "error" state.register.state = "error"
if (error instanceof ZodError) { if (error instanceof ZodError) {
state.register.errorsFields = state.register.errors.fields =
zodPresenter.getErrorsFieldsFromZodError<UserRegisterData>(error) getErrorsFieldsFromZodError<UserRegisterData>(error)
} else { } else {
state.register.errorGlobal = "unknown" state.register.errors.global = "unknown"
} }
}) })
} }
@ -96,8 +106,10 @@ export class AuthenticationPresenter
try { try {
this.setState((state) => { this.setState((state) => {
state.login.state = "loading" state.login.state = "loading"
state.login.errorsFields = [] state.login.errors = {
state.login.errorGlobal = null fields: [],
global: null,
}
}) })
const user = await this.authenticationUseCase.login(data) const user = await this.authenticationUseCase.login(data)
this.setState((state) => { this.setState((state) => {
@ -108,10 +120,10 @@ export class AuthenticationPresenter
this.setState((state) => { this.setState((state) => {
state.login.state = "error" state.login.state = "error"
if (error instanceof ZodError) { if (error instanceof ZodError) {
state.login.errorsFields = state.login.errors.fields =
zodPresenter.getErrorsFieldsFromZodError<UserLoginData>(error) getErrorsFieldsFromZodError<UserLoginData>(error)
} else { } else {
state.login.errorGlobal = "unknown" state.login.errors.global = "unknown"
} }
}) })
} }

View File

@ -1,18 +0,0 @@
export const colorsPresenter = {
hexToRgbA: (hexColor: string, opacity: number): string => {
let hex = hexColor.replace("#", "")
if (hex.length === 3) {
hex = hex
.split("")
.map((char) => {
return char + char
})
.join("")
}
const color = Number.parseInt(hex, 16)
const red = (color >> 16) & 255
const green = (color >> 8) & 255
const blue = color & 255
return `rgba(${red}, ${green}, ${blue}, ${opacity})`
},
}

View File

@ -1,7 +0,0 @@
import type { ZodError } from "zod"
export const zodPresenter = {
getErrorsFieldsFromZodError: <T>(error: ZodError<T>): Array<keyof T> => {
return Object.keys(error.format()) as Array<keyof T>
},
}

View File

@ -0,0 +1,24 @@
interface GetColorRGBAFromHexOptions {
hexColor: string
opacity: number
}
export const getColorRGBAFromHex = (
options: GetColorRGBAFromHexOptions,
): string => {
const { hexColor, opacity } = options
let hex = hexColor.replace("#", "")
if (hex.length === 3) {
hex = hex
.split("")
.map((char) => {
return char + char
})
.join("")
}
const color = Number.parseInt(hex, 16)
const red = (color >> 16) & 255
const green = (color >> 8) & 255
const blue = color & 255
return `rgba(${red}, ${green}, ${blue}, ${opacity})`
}

View File

@ -0,0 +1,7 @@
import type { ZodError } from "zod"
export const getErrorsFieldsFromZodError = <T>(
error: ZodError<T>,
): Array<keyof T> => {
return Object.keys(error.format()) as Array<keyof T>
}

View File

@ -0,0 +1,68 @@
import FontAwesome6 from "@expo/vector-icons/FontAwesome6"
import { useRouter } from "expo-router"
import { List } from "react-native-paper"
import type { HabitHistory as HabitHistoryType } from "@/domain/entities/HabitHistory"
import { getColorRGBAFromHex } from "@/presentation/presenters/utils/colors"
export interface HabitHistoryProps {
habitHistory: HabitHistoryType
}
export const HabitHistory: React.FC<HabitHistoryProps> = (props) => {
const { habitHistory } = props
const { habit } = habitHistory
const router = useRouter()
const habitColor = getColorRGBAFromHex({
hexColor: habit.color,
opacity: 0.4,
})
return (
<List.Item
onPress={() => {
router.push({
pathname: "/application/habits/[habitId]/",
params: {
habitId: habit.id,
},
})
}}
title={habit.name}
style={[
{
paddingVertical: 20,
paddingHorizontal: 10,
marginVertical: 10,
borderRadius: 10,
backgroundColor: habitColor,
},
]}
contentStyle={[
{
paddingLeft: 12,
},
]}
titleStyle={[
{
fontSize: 18,
},
]}
left={() => {
return (
<FontAwesome6
size={24}
name={habit.icon}
style={[
{
width: 30,
},
]}
/>
)
}}
/>
)
}

View File

@ -0,0 +1,30 @@
import { FlatList } from "react-native"
import { List } from "react-native-paper"
import type { HabitHistory as HabitHistoryType } from "@/domain/entities/HabitHistory"
import { HabitHistory } from "./HabitHistory"
export interface HabitsHistoryProps {
habitsHistory: HabitHistoryType[]
}
export const HabitsHistory: React.FC<HabitsHistoryProps> = (props) => {
const { habitsHistory } = props
return (
<List.Section
style={[
{
width: "90%",
},
]}
>
<FlatList
data={habitsHistory}
renderItem={({ item }) => {
return <HabitHistory habitHistory={item} />
}}
/>
</List.Section>
)
}

View File

@ -9,5 +9,5 @@ export const TabBarIcon: React.FC<{
name: React.ComponentProps<typeof FontAwesome>["name"] name: React.ComponentProps<typeof FontAwesome>["name"]
color: string color: string
}> = (props) => { }> = (props) => {
return <FontAwesome size={28} style={{ marginBottom: -3 }} {...props} /> return <FontAwesome size={28} style={[{ marginBottom: -3 }]} {...props} />
} }