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

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
- [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.
- [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 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.
<!--
- [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
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
```
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

View File

@ -7,6 +7,10 @@ export type GoalType = (typeof GOAL_TYPES)[number]
interface GoalBase {
frequency: GoalFrequency
}
export interface GoalBaseJSON extends GoalBase {
type: GoalType
}
export abstract class Goal implements GoalBase {
public frequency: GoalBase["frequency"]
public abstract readonly type: GoalType
@ -29,14 +33,18 @@ export abstract class Goal implements GoalBase {
public isBoolean(): this is GoalBoolean {
return Goal.isBoolean(this)
}
public abstract toJSON(): GoalBaseJSON
}
interface GoalProgressBase {
export interface GoalProgressBase {
goal: Goal
}
export abstract class GoalProgress implements GoalProgressBase {
public abstract readonly goal: Goal
public abstract isCompleted(): boolean
public abstract toJSON(): GoalProgressBase
}
interface GoalNumericOptions extends GoalBase {
@ -57,6 +65,14 @@ export class GoalNumeric extends Goal {
const { target } = options
this.target = target
}
public override toJSON(): GoalNumericOptions & GoalBaseJSON {
return {
frequency: this.frequency,
target: this.target,
type: this.type,
}
}
}
interface GoalNumericProgressOptions extends GoalProgressBase {
goal: GoalNumeric
@ -82,10 +98,24 @@ export class GoalNumericProgress extends GoalProgress {
? 0
: this.progress / this.goal.target.value
}
public override toJSON(): GoalNumericProgressOptions {
return {
goal: this.goal,
progress: this.progress,
}
}
}
export class GoalBoolean extends Goal {
public readonly type = "boolean"
public override toJSON(): GoalBaseJSON {
return {
frequency: this.frequency,
type: this.type,
}
}
}
interface GoalBooleanProgressOptions extends GoalProgressBase {
goal: GoalBoolean
@ -105,4 +135,11 @@ export class GoalBooleanProgress extends GoalProgress {
public override isCompleted(): boolean {
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 type { User } from "./User"
import type { EntityOptions } from "./_Entity"
import { Entity } from "./_Entity"
import { z } from "zod"
export interface HabitOptions extends EntityOptions {
userId: User["id"]
name: string
color: string
icon: string
import type { Goal, GoalBaseJSON } from "./Goal"
import { Entity, EntitySchema } from "./_Entity"
export const HabitSchema = EntitySchema.extend({
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
startDate: Date
endDate?: Date
}
export class Habit extends Entity implements HabitOptions {
public userId: HabitOptions["userId"]
public name: HabitOptions["name"]
public color: HabitOptions["color"]
public icon: HabitOptions["icon"]
public goal: HabitOptions["goal"]
public startDate: HabitOptions["startDate"]
public endDate?: HabitOptions["endDate"]
export interface HabitJSON extends HabitDataBase {
goal: GoalBaseJSON
startDate: string
endDate?: string
}
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
super({ id })
this.userId = userId
@ -33,4 +47,17 @@ export class Habit extends Entity implements HabitOptions {
this.startDate = startDate
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 { HabitProgress } from "./HabitProgress"
export interface HabitHistoryOptions {
export interface HabitHistoryJSON {
habit: Habit
progressHistory: HabitProgress[]
}
export class HabitHistory implements HabitHistoryOptions {
export class HabitHistory implements HabitHistoryJSON {
public habit: Habit
public progressHistory: HabitProgress[]
public constructor(options: HabitHistoryOptions) {
public constructor(options: HabitHistoryJSON) {
const { habit, progressHistory } = options
this.habit = habit
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 { EntityOptions } from "./_Entity"
import type { EntityData } from "./_Entity"
import { Entity } from "./_Entity"
export interface HabitProgressOptions extends EntityOptions {
interface HabitProgressDataBase extends EntityData {
habitId: Habit["id"]
}
export interface HabitProgressData extends HabitProgressDataBase {
goalProgress: GoalProgress
date: Date
}
export class HabitProgress extends Entity implements HabitProgressOptions {
public habitId: HabitProgressOptions["habitId"]
public goalProgress: HabitProgressOptions["goalProgress"]
public date: HabitProgressOptions["date"]
export interface HabitProgressJSON extends HabitProgressDataBase {
goalProgress: GoalProgressBase
date: string
}
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
super({ id })
this.habitId = habitId
this.goalProgress = goalProgress
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"
export interface HabitsTrackerOptions {
export interface HabitsTrackerData {
habitsHistory: HabitHistory[]
}
export class HabitsTracker implements HabitsTrackerOptions {
export class HabitsTracker implements HabitsTrackerData {
public habitsHistory: HabitHistory[]
public constructor(options: HabitsTrackerOptions) {
public constructor(options: HabitsTrackerData) {
const { habitsHistory } = options
this.habitsHistory = habitsHistory
}

View File

@ -1,19 +1,35 @@
import type { EntityOptions } from "./_Entity"
import { Entity } from "./_Entity"
import { z } from "zod"
export interface UserOptions extends EntityOptions {
email: string
displayName: string
}
import { Entity, EntitySchema } from "./_Entity"
export class User extends Entity implements UserOptions {
public email: UserOptions["email"]
public displayName: UserOptions["displayName"]
export const UserSchema = EntitySchema.extend({
email: z.string().min(1).email(),
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
super({ id })
this.email = email
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 {
id: string
}
import { z } from "zod"
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 constructor(options: EntityOptions) {
public constructor(options: EntityData) {
const { id } = options
this.id = id
}
// public equals(entity: Entity): boolean {
// return entity.id === this.id
// }
public equals(entity: Entity): boolean {
return entity.id === this.id
}
public abstract toJSON(): EntityData
}

View File

@ -36,7 +36,12 @@ export class RetrieveHabitsTrackerUseCase
await this.getHabitProgressHistoryRepository.execute({
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({

36
package-lock.json generated
View File

@ -10,6 +10,7 @@
"hasInstallScript": true,
"dependencies": {
"@expo/vector-icons": "14.0.0",
"@hookform/resolvers": "3.3.4",
"@react-navigation/native": "6.1.16",
"@supabase/supabase-js": "2.39.8",
"expo": "50.0.13",
@ -23,13 +24,15 @@
"immer": "10.0.4",
"react": "18.2.0",
"react-dom": "18.2.0",
"react-hook-form": "7.51.1",
"react-native": "0.73.5",
"react-native-calendars": "1.1304.1",
"react-native-paper": "5.12.3",
"react-native-safe-area-context": "4.8.2",
"react-native-screens": "3.29.0",
"react-native-vector-icons": "10.0.3",
"react-native-web": "0.19.10"
"react-native-web": "0.19.10",
"zod": "3.22.4"
},
"devDependencies": {
"@babel/core": "7.24.0",
@ -4261,6 +4264,14 @@
"@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": {
"version": "0.11.14",
"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"
}
},
"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": {
"version": "16.13.1",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
@ -23183,6 +23209,14 @@
"funding": {
"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:staged": "lint-staged",
"test": "jest --coverage --reporters=default --reporters=jest-junit",
"supabase": "supabase --workdir \"./data/infrastructure/repositories\"",
"supabase": "supabase --workdir \"./infrastructure/repositories\"",
"postinstall": "husky"
},
"dependencies": {
"@expo/vector-icons": "14.0.0",
"@hookform/resolvers": "3.3.4",
"@react-navigation/native": "6.1.16",
"@supabase/supabase-js": "2.39.8",
"expo": "50.0.13",
@ -32,13 +33,15 @@
"immer": "10.0.4",
"react": "18.2.0",
"react-dom": "18.2.0",
"react-hook-form": "7.51.1",
"react-native": "0.73.5",
"react-native-calendars": "1.1304.1",
"react-native-paper": "5.12.3",
"react-native-safe-area-context": "4.8.2",
"react-native-screens": "3.29.0",
"react-native-vector-icons": "10.0.3",
"react-native-web": "0.19.10"
"react-native-web": "0.19.10",
"zod": "3.22.4"
},
"devDependencies": {
"@babel/core": "7.24.0",