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
name="habits/history"
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 { StyleSheet } from "react-native"
import { Calendar } from "react-native-calendars"
import { SafeAreaView } from "react-native-safe-area-context"
@ -7,7 +6,15 @@ const HistoryPage: React.FC = () => {
const [selected, setSelected] = useState("")
return (
<SafeAreaView style={styles.container}>
<SafeAreaView
style={[
{
flex: 1,
alignItems: "center",
justifyContent: "center",
},
]}
>
<Calendar
onDayPress={(day) => {
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

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 { HabitsHistory } from "@/presentation/react/components/HabitsHistory/HabitsHistory"
import { useHabitsTracker } from "@/presentation/react/contexts/HabitsTracker"
import { colorsPresenter } from "@/presentation/presenters/utils/ColorsPresenter"
const HabitsPage: React.FC = () => {
const { habitsTracker } = useHabitsTracker()
return (
<SafeAreaView style={[styles.container]}>
<List.Section style={[styles.habitsList]}>
<FlatList
data={habitsTracker.habitsHistory}
renderItem={({ item }) => {
const { habit } = item
return (
<List.Item
title={habit.name}
style={[
styles.habitItem,
{
backgroundColor: colorsPresenter.hexToRgbA(
habit.color,
0.4,
),
},
]}
contentStyle={[
{
paddingLeft: 12,
},
]}
titleStyle={[
{
fontSize: 18,
},
]}
left={() => {
return (
<FontAwesome6
size={24}
name={habit.icon}
style={[styles.habitItemIcon]}
/>
)
}}
/>
)
}}
/>
</List.Section>
<SafeAreaView
style={[
{
flex: 1,
alignItems: "center",
},
]}
>
<HabitsHistory habitsHistory={habitsTracker.habitsHistory} />
</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

View File

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

View File

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

View File

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

View File

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

View File

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