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:
parent
39ebe3a152
commit
1c648972d5
@ -36,6 +36,12 @@ const TabLayout: React.FC = () => {
|
||||
},
|
||||
}}
|
||||
/>
|
||||
<Tabs.Screen
|
||||
name="habits/[habitId]"
|
||||
options={{
|
||||
href: null,
|
||||
}}
|
||||
/>
|
||||
<Tabs.Screen
|
||||
name="habits/history"
|
||||
options={{
|
||||
|
7
app/application/habits/[habitId]/_layout.tsx
Normal file
7
app/application/habits/[habitId]/_layout.tsx
Normal file
@ -0,0 +1,7 @@
|
||||
import { Slot } from "expo-router"
|
||||
|
||||
const HabitLayout: React.FC = () => {
|
||||
return <Slot />
|
||||
}
|
||||
|
||||
export default HabitLayout
|
22
app/application/habits/[habitId]/index.tsx
Normal file
22
app/application/habits/[habitId]/index.tsx
Normal 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
|
@ -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
|
||||
|
@ -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}
|
||||
<SafeAreaView
|
||||
style={[
|
||||
styles.habitItem,
|
||||
{
|
||||
backgroundColor: colorsPresenter.hexToRgbA(
|
||||
habit.color,
|
||||
0.4,
|
||||
),
|
||||
flex: 1,
|
||||
alignItems: "center",
|
||||
},
|
||||
]}
|
||||
contentStyle={[
|
||||
{
|
||||
paddingLeft: 12,
|
||||
},
|
||||
]}
|
||||
titleStyle={[
|
||||
{
|
||||
fontSize: 18,
|
||||
},
|
||||
]}
|
||||
left={() => {
|
||||
return (
|
||||
<FontAwesome6
|
||||
size={24}
|
||||
name={habit.icon}
|
||||
style={[styles.habitItemIcon]}
|
||||
/>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
</List.Section>
|
||||
>
|
||||
<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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
@ -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"
|
||||
}
|
||||
})
|
||||
}
|
||||
|
@ -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})`
|
||||
},
|
||||
}
|
@ -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>
|
||||
},
|
||||
}
|
24
presentation/presenters/utils/colors.ts
Normal file
24
presentation/presenters/utils/colors.ts
Normal 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})`
|
||||
}
|
7
presentation/presenters/utils/zod.ts
Normal file
7
presentation/presenters/utils/zod.ts
Normal 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>
|
||||
}
|
68
presentation/react/components/HabitsHistory/HabitHistory.tsx
Normal file
68
presentation/react/components/HabitsHistory/HabitHistory.tsx
Normal 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,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
@ -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>
|
||||
)
|
||||
}
|
@ -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} />
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user