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
|
<Tabs.Screen
|
||||||
name="habits/history"
|
name="habits/history"
|
||||||
options={{
|
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 { 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
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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>
|
||||||
|
@ -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>
|
||||||
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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"
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -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"]
|
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} />
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user