feat: add react-hook-form + zod

This commit is contained in:
Théo LUDWIG 2024-03-22 10:23:28 +01:00
parent bfaada5b4f
commit 90c8c7547f
Signed by: theoludwig
GPG Key ID: ADFE5A563D718F3B
12 changed files with 222 additions and 57 deletions

View File

@ -22,9 +22,10 @@ Un tracker d'habitudes pour performer au boulot et dans la vie de tous les jours
#### Principaux Outils Informatiques Utilisés #### Principaux Outils Informatiques Utilisés
- [React Native](https://reactnative.dev/) + [Expo](https://expo.io/): Framework pour le développement d'applications mobiles.
- [TypeScript](https://www.typescriptlang.org/): Langage de programmation. - [TypeScript](https://www.typescriptlang.org/): Langage de programmation.
- [React Native](https://reactnative.dev/) + [Expo](https://expo.io/): Framework pour le développement d'applications mobiles.
- [React Native Paper](https://callstack.github.io/react-native-paper/): Bibliothèque de composants pour React Native. - [React Native Paper](https://callstack.github.io/react-native-paper/): Bibliothèque de composants pour React Native.
- [React Hook Form](https://react-hook-form.com/) + [Zod](https://zod.dev/): Gestion des formulaires et validation des données.
- [Supabase](https://supabase.io/): Backend, serveur d'API pour le stockage des données. - [Supabase](https://supabase.io/): Backend, serveur d'API pour le stockage des données.
<!-- <!--
- [WatermelonDB](https://nozbe.github.io/WatermelonDB/): Base de données locale, pour permettre une utilisation hors-ligne de l'application. - [WatermelonDB](https://nozbe.github.io/WatermelonDB/): Base de données locale, pour permettre une utilisation hors-ligne de l'application.
@ -68,3 +69,22 @@ Ce n'est pas strictement nécessaire pour le développement de l'application (m
```sh ```sh
npm run supabase npm run supabase
``` ```
#### Principales Commandes Supabase
```sh
# Pour réinitialiser la base de données avec les données de test (seed.sql)
npm run supabase db reset
# Pour synchroniser la base de données (remote) avec le modèle (local)
npm run supabase db pull
# Pour synchroniser le modèle (local) avec la base de données (remote)
npm run supabase db push
# Pour générer les types TypeScript
npm run supabase gen types typescript -- --local > ./infrastructure/repositories/supabase/supabase-types.ts
# Crée un nouveau script de migration à partir des modifications déjà appliquées à votre base de données locale (remplacer `<name-of-migration>` avec le nom de la migration)
npm run supabase db diff -- -f <name-of-migration>
```

View File

@ -16,7 +16,7 @@ npm run lint:typescript
npm run test npm run test
``` ```
Une pipeline CI ([`.gitlab-ci.yml`](.gitlab-ci.yml)) est en place pour vérifier que le code respecte ces bonnes pratiques et que les tests passent. Une pipeline CI ([`.gitlab-ci.yml`](../.gitlab-ci.yml)) est en place pour vérifier que le code respecte ces bonnes pratiques et que les tests passent.
## GitFlow ## GitFlow

View File

@ -7,6 +7,10 @@ export type GoalType = (typeof GOAL_TYPES)[number]
interface GoalBase { interface GoalBase {
frequency: GoalFrequency frequency: GoalFrequency
} }
export interface GoalBaseJSON extends GoalBase {
type: GoalType
}
export abstract class Goal implements GoalBase { export abstract class Goal implements GoalBase {
public frequency: GoalBase["frequency"] public frequency: GoalBase["frequency"]
public abstract readonly type: GoalType public abstract readonly type: GoalType
@ -29,14 +33,18 @@ export abstract class Goal implements GoalBase {
public isBoolean(): this is GoalBoolean { public isBoolean(): this is GoalBoolean {
return Goal.isBoolean(this) return Goal.isBoolean(this)
} }
public abstract toJSON(): GoalBaseJSON
} }
interface GoalProgressBase { export interface GoalProgressBase {
goal: Goal goal: Goal
} }
export abstract class GoalProgress implements GoalProgressBase { export abstract class GoalProgress implements GoalProgressBase {
public abstract readonly goal: Goal public abstract readonly goal: Goal
public abstract isCompleted(): boolean public abstract isCompleted(): boolean
public abstract toJSON(): GoalProgressBase
} }
interface GoalNumericOptions extends GoalBase { interface GoalNumericOptions extends GoalBase {
@ -57,6 +65,14 @@ export class GoalNumeric extends Goal {
const { target } = options const { target } = options
this.target = target this.target = target
} }
public override toJSON(): GoalNumericOptions & GoalBaseJSON {
return {
frequency: this.frequency,
target: this.target,
type: this.type,
}
}
} }
interface GoalNumericProgressOptions extends GoalProgressBase { interface GoalNumericProgressOptions extends GoalProgressBase {
goal: GoalNumeric goal: GoalNumeric
@ -82,10 +98,24 @@ export class GoalNumericProgress extends GoalProgress {
? 0 ? 0
: this.progress / this.goal.target.value : this.progress / this.goal.target.value
} }
public override toJSON(): GoalNumericProgressOptions {
return {
goal: this.goal,
progress: this.progress,
}
}
} }
export class GoalBoolean extends Goal { export class GoalBoolean extends Goal {
public readonly type = "boolean" public readonly type = "boolean"
public override toJSON(): GoalBaseJSON {
return {
frequency: this.frequency,
type: this.type,
}
}
} }
interface GoalBooleanProgressOptions extends GoalProgressBase { interface GoalBooleanProgressOptions extends GoalProgressBase {
goal: GoalBoolean goal: GoalBoolean
@ -105,4 +135,11 @@ export class GoalBooleanProgress extends GoalProgress {
public override isCompleted(): boolean { public override isCompleted(): boolean {
return this.progress return this.progress
} }
public override toJSON(): GoalBooleanProgressOptions {
return {
goal: this.goal,
progress: this.progress,
}
}
} }

View File

@ -1,28 +1,42 @@
import type { Goal } from "./Goal" import { z } from "zod"
import type { User } from "./User"
import type { EntityOptions } from "./_Entity"
import { Entity } from "./_Entity"
export interface HabitOptions extends EntityOptions { import type { Goal, GoalBaseJSON } from "./Goal"
userId: User["id"] import { Entity, EntitySchema } from "./_Entity"
name: string
color: string export const HabitSchema = EntitySchema.extend({
icon: string userId: z.string(),
name: z.string().min(1).max(50),
color: z.string().min(4).max(9).regex(/^#/),
icon: z.string().min(1),
})
export const HabitCreateSchema = HabitSchema.extend({}).omit({ id: true })
export type HabitCreateData = z.infer<typeof HabitCreateSchema>
type HabitDataBase = z.infer<typeof HabitSchema>
export interface HabitData extends HabitDataBase {
goal: Goal goal: Goal
startDate: Date startDate: Date
endDate?: Date endDate?: Date
} }
export class Habit extends Entity implements HabitOptions { export interface HabitJSON extends HabitDataBase {
public userId: HabitOptions["userId"] goal: GoalBaseJSON
public name: HabitOptions["name"] startDate: string
public color: HabitOptions["color"] endDate?: string
public icon: HabitOptions["icon"] }
public goal: HabitOptions["goal"]
public startDate: HabitOptions["startDate"]
public endDate?: HabitOptions["endDate"]
public constructor(options: HabitOptions) { export class Habit extends Entity implements HabitData {
public userId: HabitData["userId"]
public name: HabitData["name"]
public color: HabitData["color"]
public icon: HabitData["icon"]
public goal: HabitData["goal"]
public startDate: HabitData["startDate"]
public endDate?: HabitData["endDate"]
public constructor(options: HabitData) {
const { id, userId, name, color, icon, goal, startDate, endDate } = options const { id, userId, name, color, icon, goal, startDate, endDate } = options
super({ id }) super({ id })
this.userId = userId this.userId = userId
@ -33,4 +47,17 @@ export class Habit extends Entity implements HabitOptions {
this.startDate = startDate this.startDate = startDate
this.endDate = endDate this.endDate = endDate
} }
public override toJSON(): HabitJSON {
return {
id: this.id,
userId: this.userId,
name: this.name,
color: this.color,
icon: this.icon,
goal: this.goal,
startDate: this.startDate.toISOString(),
endDate: this.endDate?.toISOString(),
}
}
} }

View File

@ -1,16 +1,16 @@
import type { Habit } from "./Habit" import type { Habit } from "./Habit"
import type { HabitProgress } from "./HabitProgress" import type { HabitProgress } from "./HabitProgress"
export interface HabitHistoryOptions { export interface HabitHistoryJSON {
habit: Habit habit: Habit
progressHistory: HabitProgress[] progressHistory: HabitProgress[]
} }
export class HabitHistory implements HabitHistoryOptions { export class HabitHistory implements HabitHistoryJSON {
public habit: Habit public habit: Habit
public progressHistory: HabitProgress[] public progressHistory: HabitProgress[]
public constructor(options: HabitHistoryOptions) { public constructor(options: HabitHistoryJSON) {
const { habit, progressHistory } = options const { habit, progressHistory } = options
this.habit = habit this.habit = habit
this.progressHistory = progressHistory this.progressHistory = progressHistory

View File

@ -1,24 +1,41 @@
import type { GoalProgress } from "./Goal" import type { GoalProgress, GoalProgressBase } from "./Goal"
import type { Habit } from "./Habit" import type { Habit } from "./Habit"
import type { EntityOptions } from "./_Entity" import type { EntityData } from "./_Entity"
import { Entity } from "./_Entity" import { Entity } from "./_Entity"
export interface HabitProgressOptions extends EntityOptions { interface HabitProgressDataBase extends EntityData {
habitId: Habit["id"] habitId: Habit["id"]
}
export interface HabitProgressData extends HabitProgressDataBase {
goalProgress: GoalProgress goalProgress: GoalProgress
date: Date date: Date
} }
export class HabitProgress extends Entity implements HabitProgressOptions { export interface HabitProgressJSON extends HabitProgressDataBase {
public habitId: HabitProgressOptions["habitId"] goalProgress: GoalProgressBase
public goalProgress: HabitProgressOptions["goalProgress"] date: string
public date: HabitProgressOptions["date"] }
public constructor(options: HabitProgressOptions) { export class HabitProgress extends Entity implements HabitProgressData {
public habitId: HabitProgressData["habitId"]
public goalProgress: HabitProgressData["goalProgress"]
public date: HabitProgressData["date"]
public constructor(options: HabitProgressData) {
const { id, habitId, goalProgress, date } = options const { id, habitId, goalProgress, date } = options
super({ id }) super({ id })
this.habitId = habitId this.habitId = habitId
this.goalProgress = goalProgress this.goalProgress = goalProgress
this.date = date this.date = date
} }
public override toJSON(): HabitProgressJSON {
return {
id: this.id,
habitId: this.habitId,
goalProgress: this.goalProgress,
date: this.date.toISOString(),
}
}
} }

View File

@ -1,13 +1,13 @@
import type { HabitHistory } from "./HabitHistory" import type { HabitHistory } from "./HabitHistory"
export interface HabitsTrackerOptions { export interface HabitsTrackerData {
habitsHistory: HabitHistory[] habitsHistory: HabitHistory[]
} }
export class HabitsTracker implements HabitsTrackerOptions { export class HabitsTracker implements HabitsTrackerData {
public habitsHistory: HabitHistory[] public habitsHistory: HabitHistory[]
public constructor(options: HabitsTrackerOptions) { public constructor(options: HabitsTrackerData) {
const { habitsHistory } = options const { habitsHistory } = options
this.habitsHistory = habitsHistory this.habitsHistory = habitsHistory
} }

View File

@ -1,19 +1,35 @@
import type { EntityOptions } from "./_Entity" import { z } from "zod"
import { Entity } from "./_Entity"
export interface UserOptions extends EntityOptions { import { Entity, EntitySchema } from "./_Entity"
email: string
displayName: string
}
export class User extends Entity implements UserOptions { export const UserSchema = EntitySchema.extend({
public email: UserOptions["email"] email: z.string().min(1).email(),
public displayName: UserOptions["displayName"] displayName: z.string().min(1),
})
public constructor(options: UserOptions) { export const UserRegisterSchema = UserSchema.extend({
password: z.string().min(2),
}).omit({ id: true })
export type UserRegisterData = z.infer<typeof UserRegisterSchema>
export type UserData = z.infer<typeof UserSchema>
export class User extends Entity implements UserData {
public email: UserData["email"]
public displayName: UserData["displayName"]
public constructor(options: UserData) {
const { id, email, displayName } = options const { id, email, displayName } = options
super({ id }) super({ id })
this.email = email this.email = email
this.displayName = displayName this.displayName = displayName
} }
public override toJSON(): UserData {
return {
id: this.id,
email: this.email,
displayName: this.displayName,
}
}
} }

View File

@ -1,16 +1,22 @@
export interface EntityOptions { import { z } from "zod"
id: string
}
export abstract class Entity implements EntityOptions { export const EntitySchema = z.object({
id: z.string(),
})
export type EntityData = z.infer<typeof EntitySchema>
export abstract class Entity implements EntityData {
public readonly id: string public readonly id: string
public constructor(options: EntityOptions) { public constructor(options: EntityData) {
const { id } = options const { id } = options
this.id = id this.id = id
} }
// public equals(entity: Entity): boolean { public equals(entity: Entity): boolean {
// return entity.id === this.id return entity.id === this.id
// } }
public abstract toJSON(): EntityData
} }

View File

@ -36,7 +36,12 @@ export class RetrieveHabitsTrackerUseCase
await this.getHabitProgressHistoryRepository.execute({ await this.getHabitProgressHistoryRepository.execute({
habit, habit,
}) })
return new HabitHistory({ habit, progressHistory }) return new HabitHistory({
habit,
progressHistory: progressHistory.sort((a, b) => {
return a.date.getTime() - b.date.getTime()
}),
})
}), }),
) )
const habitsTracker = new HabitsTracker({ const habitsTracker = new HabitsTracker({

36
package-lock.json generated
View File

@ -10,6 +10,7 @@
"hasInstallScript": true, "hasInstallScript": true,
"dependencies": { "dependencies": {
"@expo/vector-icons": "14.0.0", "@expo/vector-icons": "14.0.0",
"@hookform/resolvers": "3.3.4",
"@react-navigation/native": "6.1.16", "@react-navigation/native": "6.1.16",
"@supabase/supabase-js": "2.39.8", "@supabase/supabase-js": "2.39.8",
"expo": "50.0.13", "expo": "50.0.13",
@ -23,13 +24,15 @@
"immer": "10.0.4", "immer": "10.0.4",
"react": "18.2.0", "react": "18.2.0",
"react-dom": "18.2.0", "react-dom": "18.2.0",
"react-hook-form": "7.51.1",
"react-native": "0.73.5", "react-native": "0.73.5",
"react-native-calendars": "1.1304.1", "react-native-calendars": "1.1304.1",
"react-native-paper": "5.12.3", "react-native-paper": "5.12.3",
"react-native-safe-area-context": "4.8.2", "react-native-safe-area-context": "4.8.2",
"react-native-screens": "3.29.0", "react-native-screens": "3.29.0",
"react-native-vector-icons": "10.0.3", "react-native-vector-icons": "10.0.3",
"react-native-web": "0.19.10" "react-native-web": "0.19.10",
"zod": "3.22.4"
}, },
"devDependencies": { "devDependencies": {
"@babel/core": "7.24.0", "@babel/core": "7.24.0",
@ -4261,6 +4264,14 @@
"@hapi/hoek": "^9.0.0" "@hapi/hoek": "^9.0.0"
} }
}, },
"node_modules/@hookform/resolvers": {
"version": "3.3.4",
"resolved": "https://registry.npmjs.org/@hookform/resolvers/-/resolvers-3.3.4.tgz",
"integrity": "sha512-o5cgpGOuJYrd+iMKvkttOclgwRW86EsWJZZRC23prf0uU2i48Htq4PuT73AVb9ionFyZrwYEITuOFGF+BydEtQ==",
"peerDependencies": {
"react-hook-form": "^7.0.0"
}
},
"node_modules/@humanwhocodes/config-array": { "node_modules/@humanwhocodes/config-array": {
"version": "0.11.14", "version": "0.11.14",
"resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.14.tgz", "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.14.tgz",
@ -19905,6 +19916,21 @@
"react-dom": "^16.6.0 || ^17.0.0 || ^18.0.0" "react-dom": "^16.6.0 || ^17.0.0 || ^18.0.0"
} }
}, },
"node_modules/react-hook-form": {
"version": "7.51.1",
"resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.51.1.tgz",
"integrity": "sha512-ifnBjl+kW0ksINHd+8C/Gp6a4eZOdWyvRv0UBaByShwU8JbVx5hTcTWEcd5VdybvmPTATkVVXk9npXArHmo56w==",
"engines": {
"node": ">=12.22.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/react-hook-form"
},
"peerDependencies": {
"react": "^16.8.0 || ^17 || ^18"
}
},
"node_modules/react-is": { "node_modules/react-is": {
"version": "16.13.1", "version": "16.13.1",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
@ -23183,6 +23209,14 @@
"funding": { "funding": {
"url": "https://github.com/sponsors/sindresorhus" "url": "https://github.com/sponsors/sindresorhus"
} }
},
"node_modules/zod": {
"version": "3.22.4",
"resolved": "https://registry.npmjs.org/zod/-/zod-3.22.4.tgz",
"integrity": "sha512-iC+8Io04lddc+mVqQ9AZ7OQ2MrUKGN+oIQyq1vemgt46jwCwLfhq7/pwnBnNXXXZb8VTVLKwp9EDkx+ryxIWmg==",
"funding": {
"url": "https://github.com/sponsors/colinhacks"
}
} }
} }
} }

View File

@ -14,11 +14,12 @@
"lint:typescript": "tsc --noEmit", "lint:typescript": "tsc --noEmit",
"lint:staged": "lint-staged", "lint:staged": "lint-staged",
"test": "jest --coverage --reporters=default --reporters=jest-junit", "test": "jest --coverage --reporters=default --reporters=jest-junit",
"supabase": "supabase --workdir \"./data/infrastructure/repositories\"", "supabase": "supabase --workdir \"./infrastructure/repositories\"",
"postinstall": "husky" "postinstall": "husky"
}, },
"dependencies": { "dependencies": {
"@expo/vector-icons": "14.0.0", "@expo/vector-icons": "14.0.0",
"@hookform/resolvers": "3.3.4",
"@react-navigation/native": "6.1.16", "@react-navigation/native": "6.1.16",
"@supabase/supabase-js": "2.39.8", "@supabase/supabase-js": "2.39.8",
"expo": "50.0.13", "expo": "50.0.13",
@ -32,13 +33,15 @@
"immer": "10.0.4", "immer": "10.0.4",
"react": "18.2.0", "react": "18.2.0",
"react-dom": "18.2.0", "react-dom": "18.2.0",
"react-hook-form": "7.51.1",
"react-native": "0.73.5", "react-native": "0.73.5",
"react-native-calendars": "1.1304.1", "react-native-calendars": "1.1304.1",
"react-native-paper": "5.12.3", "react-native-paper": "5.12.3",
"react-native-safe-area-context": "4.8.2", "react-native-safe-area-context": "4.8.2",
"react-native-screens": "3.29.0", "react-native-screens": "3.29.0",
"react-native-vector-icons": "10.0.3", "react-native-vector-icons": "10.0.3",
"react-native-web": "0.19.10" "react-native-web": "0.19.10",
"zod": "3.22.4"
}, },
"devDependencies": { "devDependencies": {
"@babel/core": "7.24.0", "@babel/core": "7.24.0",