feat: add react-hook-form + zod
This commit is contained in:
parent
bfaada5b4f
commit
90c8c7547f
22
README.md
22
README.md
@ -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>
|
||||
```
|
||||
|
@ -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
|
||||
|
||||
|
@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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
36
package-lock.json
generated
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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",
|
||||
|
Reference in New Issue
Block a user