nico-tools 1.0.0 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (111) hide show
  1. package/bin/cli.js +9 -13
  2. package/bin/commands/create-project.js +106 -0
  3. package/bin/commands/translate.js +12 -0
  4. package/bin/config/templates.js +9 -0
  5. package/bin/utils/file-utils.js +84 -0
  6. package/bin/utils/project-utils.js +49 -0
  7. package/package.json +4 -2
  8. package/templates/react-native/expo-clean-architecture/README.md +154 -0
  9. package/templates/react-native/expo-clean-architecture/app.config.ts +61 -0
  10. package/templates/react-native/expo-clean-architecture/assets/config/.gitkeep +3 -0
  11. package/templates/react-native/expo-clean-architecture/assets/fonts/SpaceMono-Regular.ttf +0 -0
  12. package/templates/react-native/expo-clean-architecture/assets/images/adaptive-icon.png +0 -0
  13. package/templates/react-native/expo-clean-architecture/assets/images/favicon.png +0 -0
  14. package/templates/react-native/expo-clean-architecture/assets/images/icon.png +0 -0
  15. package/templates/react-native/expo-clean-architecture/assets/images/partial-react-logo.png +0 -0
  16. package/templates/react-native/expo-clean-architecture/assets/images/react-logo.png +0 -0
  17. package/templates/react-native/expo-clean-architecture/assets/images/react-logo@2x.png +0 -0
  18. package/templates/react-native/expo-clean-architecture/assets/images/react-logo@3x.png +0 -0
  19. package/templates/react-native/expo-clean-architecture/assets/images/splash-icon.png +0 -0
  20. package/templates/react-native/expo-clean-architecture/babel.config.js +11 -0
  21. package/templates/react-native/expo-clean-architecture/docs/00-introduction.md +3 -0
  22. package/templates/react-native/expo-clean-architecture/docs/01-architecture.md +107 -0
  23. package/templates/react-native/expo-clean-architecture/package.json +78 -0
  24. package/templates/react-native/expo-clean-architecture/scripts/clean-src.sh +48 -0
  25. package/templates/react-native/expo-clean-architecture/scripts/generate-feature.sh +40 -0
  26. package/templates/react-native/expo-clean-architecture/src/app/(protected)/(tabs)/_layout.tsx +42 -0
  27. package/templates/react-native/expo-clean-architecture/src/app/(protected)/(tabs)/favorites.tsx +72 -0
  28. package/templates/react-native/expo-clean-architecture/src/app/(protected)/(tabs)/home.tsx +122 -0
  29. package/templates/react-native/expo-clean-architecture/src/app/(protected)/(tabs)/settings/_layout.tsx +5 -0
  30. package/templates/react-native/expo-clean-architecture/src/app/(protected)/(tabs)/settings/index.tsx +29 -0
  31. package/templates/react-native/expo-clean-architecture/src/app/(protected)/(tabs)/settings/profile.tsx +22 -0
  32. package/templates/react-native/expo-clean-architecture/src/app/(protected)/_layout.tsx +20 -0
  33. package/templates/react-native/expo-clean-architecture/src/app/(protected)/details.tsx +124 -0
  34. package/templates/react-native/expo-clean-architecture/src/app/(public)/_layout.tsx +18 -0
  35. package/templates/react-native/expo-clean-architecture/src/app/(public)/login.tsx +31 -0
  36. package/templates/react-native/expo-clean-architecture/src/app/_layout.tsx +33 -0
  37. package/templates/react-native/expo-clean-architecture/src/app/index.tsx +8 -0
  38. package/templates/react-native/expo-clean-architecture/src/core/constants/api-constants.ts +10 -0
  39. package/templates/react-native/expo-clean-architecture/src/core/constants/image-constants.ts +3 -0
  40. package/templates/react-native/expo-clean-architecture/src/core/constants/query-keys.ts +6 -0
  41. package/templates/react-native/expo-clean-architecture/src/core/constants/storage-keys.ts +3 -0
  42. package/templates/react-native/expo-clean-architecture/src/core/design/@types/color-scheme-state.ts +35 -0
  43. package/templates/react-native/expo-clean-architecture/src/core/design/@types/color-scheme.ts +12 -0
  44. package/templates/react-native/expo-clean-architecture/src/core/design/components/app-icon.tsx +16 -0
  45. package/templates/react-native/expo-clean-architecture/src/core/design/components/app-separator.tsx +26 -0
  46. package/templates/react-native/expo-clean-architecture/src/core/design/hooks/use-app-color-scheme.ts +52 -0
  47. package/templates/react-native/expo-clean-architecture/src/core/design/hooks/use-app-fonts.ts +12 -0
  48. package/templates/react-native/expo-clean-architecture/src/core/design/hooks/use-app-styles.ts +28 -0
  49. package/templates/react-native/expo-clean-architecture/src/core/design/theme/app-colors.ts +21 -0
  50. package/templates/react-native/expo-clean-architecture/src/core/design/theme/app-fonts.ts +16 -0
  51. package/templates/react-native/expo-clean-architecture/src/core/design/theme/app-sizes.ts +14 -0
  52. package/templates/react-native/expo-clean-architecture/src/core/di/injection-container.ts +53 -0
  53. package/templates/react-native/expo-clean-architecture/src/core/errors/index.ts +1 -0
  54. package/templates/react-native/expo-clean-architecture/src/core/helpers/@types.ts +23 -0
  55. package/templates/react-native/expo-clean-architecture/src/core/helpers/rest-client.ts +144 -0
  56. package/templates/react-native/expo-clean-architecture/src/core/helpers/result.ts +37 -0
  57. package/templates/react-native/expo-clean-architecture/src/core/helpers/usecase.ts +5 -0
  58. package/templates/react-native/expo-clean-architecture/src/core/hooks/use-network.ts +18 -0
  59. package/templates/react-native/expo-clean-architecture/src/core/i18n/@types/i18next.d.ts +11 -0
  60. package/templates/react-native/expo-clean-architecture/src/core/i18n/index.ts +19 -0
  61. package/templates/react-native/expo-clean-architecture/src/core/i18n/translations/fr.json +12 -0
  62. package/templates/react-native/expo-clean-architecture/src/core/services/local-storage-service-impl.ts +29 -0
  63. package/templates/react-native/expo-clean-architecture/src/core/services/local-storage-service.ts +26 -0
  64. package/templates/react-native/expo-clean-architecture/src/core/services/monitoring-service-impl.ts +15 -0
  65. package/templates/react-native/expo-clean-architecture/src/core/services/monitoring-service.ts +13 -0
  66. package/templates/react-native/expo-clean-architecture/src/core/services/network-service-impl.ts +40 -0
  67. package/templates/react-native/expo-clean-architecture/src/core/services/network-service.ts +16 -0
  68. package/templates/react-native/expo-clean-architecture/src/features/auth/@types/session-state.ts +38 -0
  69. package/templates/react-native/expo-clean-architecture/src/features/auth/@types/session-status-enum.ts +16 -0
  70. package/templates/react-native/expo-clean-architecture/src/features/auth/presentation/hooks/use-session.ts +18 -0
  71. package/templates/react-native/expo-clean-architecture/src/features/favorites/data/datasources/favorites-datasource-impl.ts +25 -0
  72. package/templates/react-native/expo-clean-architecture/src/features/favorites/data/datasources/favorites-datasource.ts +5 -0
  73. package/templates/react-native/expo-clean-architecture/src/features/favorites/data/repositories/favorites-repository-impl.ts +46 -0
  74. package/templates/react-native/expo-clean-architecture/src/features/favorites/domain/repositories/favorites-repository.ts +8 -0
  75. package/templates/react-native/expo-clean-architecture/src/features/favorites/domain/usecases/add-to-favorites-usecase.ts +23 -0
  76. package/templates/react-native/expo-clean-architecture/src/features/favorites/domain/usecases/clear-favorites-usecase.ts +23 -0
  77. package/templates/react-native/expo-clean-architecture/src/features/favorites/domain/usecases/get-favorites-usecase.ts +24 -0
  78. package/templates/react-native/expo-clean-architecture/src/features/favorites/domain/usecases/remove-from-favorites-usecase.ts +23 -0
  79. package/templates/react-native/expo-clean-architecture/src/features/favorites/presentation/components/favorites-card.tsx +77 -0
  80. package/templates/react-native/expo-clean-architecture/src/features/favorites/presentation/hooks/use-add-favorite.ts +24 -0
  81. package/templates/react-native/expo-clean-architecture/src/features/favorites/presentation/hooks/use-clear-favorites.ts +22 -0
  82. package/templates/react-native/expo-clean-architecture/src/features/favorites/presentation/hooks/use-favorites.ts +22 -0
  83. package/templates/react-native/expo-clean-architecture/src/features/favorites/presentation/hooks/use-remove-favorite.ts +24 -0
  84. package/templates/react-native/expo-clean-architecture/src/features/movies/data/datasources/movies-datasource-impl.ts +50 -0
  85. package/templates/react-native/expo-clean-architecture/src/features/movies/data/datasources/movies-datasource.ts +8 -0
  86. package/templates/react-native/expo-clean-architecture/src/features/movies/data/models/movie-details-model.ts +67 -0
  87. package/templates/react-native/expo-clean-architecture/src/features/movies/data/models/movie-model.ts +30 -0
  88. package/templates/react-native/expo-clean-architecture/src/features/movies/data/models/tmdb-response-model.ts +6 -0
  89. package/templates/react-native/expo-clean-architecture/src/features/movies/data/repositories/movies-repository-impl.ts +34 -0
  90. package/templates/react-native/expo-clean-architecture/src/features/movies/domain/entities/movie-details-entity.ts +28 -0
  91. package/templates/react-native/expo-clean-architecture/src/features/movies/domain/entities/movie-entity.ts +6 -0
  92. package/templates/react-native/expo-clean-architecture/src/features/movies/domain/repositories/movies-repository.ts +25 -0
  93. package/templates/react-native/expo-clean-architecture/src/features/movies/domain/usecases/get-movie-details-usecase.ts +26 -0
  94. package/templates/react-native/expo-clean-architecture/src/features/movies/domain/usecases/get-random-movies-usecase.ts +24 -0
  95. package/templates/react-native/expo-clean-architecture/src/features/movies/domain/usecases/search-movies-usecase.ts +23 -0
  96. package/templates/react-native/expo-clean-architecture/src/features/movies/presentation/components/movie-tile.tsx +69 -0
  97. package/templates/react-native/expo-clean-architecture/src/features/movies/presentation/hooks/use-movie-details.ts +22 -0
  98. package/templates/react-native/expo-clean-architecture/src/features/movies/presentation/hooks/use-random-movies.ts +22 -0
  99. package/templates/react-native/expo-clean-architecture/src/features/movies/presentation/hooks/use-search-movies.ts +22 -0
  100. package/templates/react-native/expo-clean-architecture/tests/core/services/local-storage-service-impl.test.ts +108 -0
  101. package/templates/react-native/expo-clean-architecture/tests/core/services/monitoring-service.test.ts +74 -0
  102. package/templates/react-native/expo-clean-architecture/tests/core/services/network-service.test.ts +117 -0
  103. package/templates/react-native/expo-clean-architecture/tests/features/auth/presentation/hooks/use-session.test.ts +69 -0
  104. package/templates/react-native/expo-clean-architecture/tests/features/favorites/data/datasources/favorites-datasource.test.ts +69 -0
  105. package/templates/react-native/expo-clean-architecture/tests/features/favorites/data/repositories/favorites-repository-impl.test.ts +124 -0
  106. package/templates/react-native/expo-clean-architecture/tests/features/favorites/domain/usecases/add-to-favorites-usecase.test.ts +54 -0
  107. package/templates/react-native/expo-clean-architecture/tests/features/favorites/domain/usecases/clear-favorites-usecase.test.ts +44 -0
  108. package/templates/react-native/expo-clean-architecture/tests/features/favorites/domain/usecases/get-favorites-usecase.test.ts +74 -0
  109. package/templates/react-native/expo-clean-architecture/tests/features/favorites/domain/usecases/remove-from-favorites-usecase.test.ts +52 -0
  110. package/templates/react-native/expo-clean-architecture/tests/setup.ts +9 -0
  111. package/templates/react-native/expo-clean-architecture/tsconfig.json +20 -0
@@ -0,0 +1,144 @@
1
+ import {
2
+ RestClientOptions,
3
+ GetRequestConfig,
4
+ PostRequestConfig,
5
+ PutRequestConfig,
6
+ DeleteRequestConfig,
7
+ RequestConfig,
8
+ } from "@/core/helpers/@types";
9
+
10
+ export abstract class RestClient {
11
+ private readonly baseUrl: string;
12
+ private readonly defaultHeaders: Record<string, string>;
13
+
14
+ constructor(options: RestClientOptions) {
15
+ this.baseUrl = options.baseUrl || "";
16
+ this.defaultHeaders = options.headers || {};
17
+ }
18
+
19
+ /**
20
+ * Perform a GET request to the server
21
+ * @param options - The options for the request
22
+ * @returns The resource
23
+ */
24
+ async get<T>(options: GetRequestConfig): Promise<T> {
25
+ try {
26
+ const response = await fetch(
27
+ this.getFullUrl(options.path, options.queryParameters),
28
+ {
29
+ ...options,
30
+ headers: this.getHeaders(options),
31
+ }
32
+ );
33
+
34
+ return this.handleResponse<T>(response);
35
+ } catch (error) {
36
+ return this.handleError(error as Error);
37
+ }
38
+ }
39
+
40
+ /**
41
+ * Perform a POST request to the server
42
+ * @param config - The config for the request
43
+ * @returns The resource
44
+ */
45
+ async post<T>(config: PostRequestConfig): Promise<T> {
46
+ try {
47
+ const response = await fetch(this.getFullUrl(config.path), {
48
+ ...config,
49
+ method: "POST",
50
+ body: JSON.stringify(config.body),
51
+ headers: this.getHeaders(config),
52
+ });
53
+ return this.handleResponse<T>(response);
54
+ } catch (error) {
55
+ return this.handleError(error as Error);
56
+ }
57
+ }
58
+
59
+ /**
60
+ * Perform a PUT request to the server
61
+ * @param config - The config for the request
62
+ * @returns The resource
63
+ */
64
+ async put<T>(config: PutRequestConfig): Promise<T> {
65
+ try {
66
+ const response = await fetch(this.getFullUrl(config.path), {
67
+ ...config,
68
+ method: "PUT",
69
+ body: JSON.stringify(config.body),
70
+ headers: this.getHeaders(config),
71
+ });
72
+ return this.handleResponse<T>(response);
73
+ } catch (error) {
74
+ return this.handleError(error as Error);
75
+ }
76
+ }
77
+
78
+ /**
79
+ * Perform a DELETE request to the server
80
+ * @param config - The config for the request
81
+ * @returns The resource
82
+ */
83
+ async delete<T>(config: DeleteRequestConfig): Promise<T> {
84
+ try {
85
+ const response = await fetch(this.getFullUrl(config.path), {
86
+ ...config,
87
+ method: "DELETE",
88
+ headers: this.getHeaders(config),
89
+ });
90
+ return this.handleResponse<T>(response);
91
+ } catch (error) {
92
+ return this.handleError(error as Error);
93
+ }
94
+ }
95
+
96
+ /**
97
+ * Get the full URL for a request
98
+ * @param path - The path of the resource
99
+ * @param queryParameters - The query parameters for the request - _Optional_
100
+ * @returns The full URL
101
+ */
102
+ private getFullUrl(
103
+ path: string,
104
+ queryParameters?: Record<string, string>
105
+ ): string {
106
+ const queryString = queryParameters
107
+ ? `?${new URLSearchParams(queryParameters).toString()}`
108
+ : "";
109
+ return `${this.baseUrl}${path}${queryString}`;
110
+ }
111
+
112
+ /**
113
+ * Concatenate the default headers with the request headers
114
+ * @param config - The config for the request
115
+ * @returns The headers
116
+ */
117
+ private getHeaders(config?: RequestConfig): Record<string, string> {
118
+ return {
119
+ ...this.defaultHeaders,
120
+ ...config?.options?.headers?.toString,
121
+ };
122
+ }
123
+
124
+ /**
125
+ * Handle the response from the server
126
+ * @param response - The response from the server
127
+ * @returns The resource as a JSON object, or throws an error if the response is not ok
128
+ */
129
+ private handleResponse<T>(response: Response): Promise<T> {
130
+ if (!response.ok) {
131
+ throw new Error(response.statusText);
132
+ }
133
+ return response.json();
134
+ }
135
+
136
+ /**
137
+ * Handle the error from the server
138
+ * @param error - The error from the server
139
+ * @returns The error
140
+ */
141
+ private handleError(error: Error): Promise<never> {
142
+ throw error;
143
+ }
144
+ }
@@ -0,0 +1,37 @@
1
+ export abstract class Result<T> {
2
+ data: T | null;
3
+ error: Error | null;
4
+
5
+ constructor(data: T, error: Error | null) {
6
+ this.data = data;
7
+ this.error = error;
8
+ }
9
+
10
+ get isSuccess(): boolean {
11
+ return this instanceof Success;
12
+ }
13
+
14
+ get isFailure(): boolean {
15
+ return this instanceof Failure;
16
+ }
17
+
18
+ static success<T>(data: T): Success<T> {
19
+ return new Success(data);
20
+ }
21
+
22
+ static failure(error: any): Failure {
23
+ return new Failure(error);
24
+ }
25
+ }
26
+
27
+ export class Success<T> extends Result<T> {
28
+ constructor(data: T) {
29
+ super(data, null);
30
+ }
31
+ }
32
+
33
+ export class Failure extends Result<null> {
34
+ constructor(error: Error) {
35
+ super(null, error);
36
+ }
37
+ }
@@ -0,0 +1,5 @@
1
+ import { Result } from "./result";
2
+
3
+ export abstract class Usecase<Input, Output> {
4
+ abstract execute(input?: Input): Promise<Result<Output>>;
5
+ }
@@ -0,0 +1,18 @@
1
+ import { useEffect, useState } from "react";
2
+ import container from "@/core/di/injection-container";
3
+ import { NetworkService } from "@/core/services/network-service";
4
+
5
+ export function useNetwork(): { isOnline: boolean } {
6
+ const networkService = container.get<NetworkService>(NetworkService);
7
+ const [isOnline, setIsOnline] = useState<boolean>(false);
8
+
9
+ useEffect(() => {
10
+ const unsubscribe = networkService.subscribe(setIsOnline);
11
+
12
+ return () => {
13
+ unsubscribe();
14
+ };
15
+ }, [networkService]);
16
+
17
+ return { isOnline };
18
+ }
@@ -0,0 +1,11 @@
1
+ import "i18next";
2
+ import fr from "@/core/i18n/translations/fr.json";
3
+
4
+ declare module "i18next" {
5
+ interface CustomTypeOptions {
6
+ defaultNS: "fr";
7
+ resources: {
8
+ fr: typeof fr;
9
+ };
10
+ }
11
+ }
@@ -0,0 +1,19 @@
1
+ import i18n from "i18next";
2
+ import { initReactI18next } from "react-i18next";
3
+ import fr from "@/core/i18n/translations/fr.json";
4
+
5
+ const resources = {
6
+ fr: {
7
+ translation: fr,
8
+ },
9
+ };
10
+
11
+ i18n.use(initReactI18next).init({
12
+ resources,
13
+ lng: "fr",
14
+ react: {
15
+ useSuspense: true,
16
+ },
17
+ });
18
+
19
+ export default i18n;
@@ -0,0 +1,12 @@
1
+ {
2
+ "common": {
3
+ "helloWorld": "Bonjour le monde"
4
+ },
5
+ "auth": {
6
+ "login": "Connexion",
7
+ "logout": "Déconnexion"
8
+ },
9
+ "home": {
10
+ "title": "Accueil"
11
+ }
12
+ }
@@ -0,0 +1,29 @@
1
+ import { injectable } from "inversify";
2
+ import AsyncStorage from "@react-native-async-storage/async-storage";
3
+
4
+ import { LocalStorageService } from "@/core/services/local-storage-service";
5
+
6
+ @injectable()
7
+ export class LocalStorageServiceImpl extends LocalStorageService {
8
+ async getItem(key: string, defaultValue: string): Promise<string> {
9
+ const value = await AsyncStorage.getItem(key);
10
+
11
+ if (value === null) {
12
+ return defaultValue;
13
+ }
14
+
15
+ return value;
16
+ }
17
+
18
+ async setItem(key: string, value: string): Promise<void> {
19
+ await AsyncStorage.setItem(key, value);
20
+ }
21
+
22
+ async removeItem(key: string): Promise<void> {
23
+ await AsyncStorage.removeItem(key);
24
+ }
25
+
26
+ async clear(): Promise<void> {
27
+ await AsyncStorage.clear();
28
+ }
29
+ }
@@ -0,0 +1,26 @@
1
+ export abstract class LocalStorageService {
2
+ /**
3
+ * Get an item from the local storage
4
+ * @param key The key of the item to get
5
+ * @returns The item or null if it doesn't exist
6
+ */
7
+ abstract getItem(key: string, defaultValue: string): Promise<string>;
8
+
9
+ /**
10
+ * Set an item in the local storage
11
+ * @param key The key of the item to set
12
+ * @param value The value of the item to set
13
+ */
14
+ abstract setItem(key: string, value: string): Promise<void>;
15
+
16
+ /**
17
+ * Remove an item from the local storage
18
+ * @param key The key of the item to remove
19
+ */
20
+ abstract removeItem(key: string): Promise<void>;
21
+
22
+ /**
23
+ * Clear the local storage
24
+ */
25
+ abstract clear(): Promise<void>;
26
+ }
@@ -0,0 +1,15 @@
1
+ import { injectable } from "inversify";
2
+ import {
3
+ recordError,
4
+ getCrashlytics,
5
+ } from "@react-native-firebase/crashlytics";
6
+
7
+ import { MonitoringService } from "@/core/services/monitoring-service";
8
+
9
+ @injectable()
10
+ export class MonitoringServiceImpl implements MonitoringService {
11
+ report(error: Error): void {
12
+ const crashlytics = getCrashlytics();
13
+ recordError(crashlytics, error);
14
+ }
15
+ }
@@ -0,0 +1,13 @@
1
+ import { injectable } from "inversify";
2
+ import {
3
+ recordError,
4
+ getCrashlytics,
5
+ } from "@react-native-firebase/crashlytics";
6
+
7
+ export abstract class MonitoringService {
8
+ /**
9
+ * Report an error to the monitoring service
10
+ * @param error - The error to report
11
+ */
12
+ abstract report(error: Error): void;
13
+ }
@@ -0,0 +1,40 @@
1
+ import { injectable } from "inversify";
2
+ import {
3
+ getNetworkStateAsync,
4
+ addNetworkStateListener,
5
+ NetworkState,
6
+ } from "expo-network";
7
+ import { NetworkService } from "@/core/services/network-service";
8
+ import { NetworkStateChangeCallback } from "@/core/services/network-service";
9
+
10
+ @injectable()
11
+ export class NetworkServiceImpl implements NetworkService {
12
+ private listeners: Set<NetworkStateChangeCallback> = new Set();
13
+
14
+ constructor() {
15
+ // Initialize the listener when the service is created
16
+ addNetworkStateListener(this.handleNetworkStateChange);
17
+ }
18
+
19
+ private handleNetworkStateChange = (state: NetworkState) => {
20
+ const isOnline = state.isInternetReachable ?? false;
21
+ this.listeners.forEach((callback) => callback(isOnline));
22
+ };
23
+
24
+ async isOnline(): Promise<boolean> {
25
+ const networkState = await getNetworkStateAsync();
26
+ return networkState.isInternetReachable ?? false;
27
+ }
28
+
29
+ subscribe(callback: NetworkStateChangeCallback): () => void {
30
+ this.listeners.add(callback);
31
+
32
+ // Initial state check
33
+ this.isOnline().then((isOnline) => callback(isOnline));
34
+
35
+ // Return unsubscribe function
36
+ return () => {
37
+ this.listeners.delete(callback);
38
+ };
39
+ }
40
+ }
@@ -0,0 +1,16 @@
1
+ export type NetworkStateChangeCallback = (isOnline: boolean) => void;
2
+
3
+ export abstract class NetworkService {
4
+ /**
5
+ * Check if the device has an internet connection
6
+ * @returns true if the device has an internet connection, false otherwise
7
+ */
8
+ abstract isOnline(): Promise<boolean>;
9
+
10
+ /**
11
+ * Subscribe to the network state change
12
+ * @param callback - The function to be called when the network state changes
13
+ * @returns A function to unsubscribe from the network state change
14
+ */
15
+ abstract subscribe(callback: NetworkStateChangeCallback): () => void;
16
+ }
@@ -0,0 +1,38 @@
1
+ import { SessionStatus } from "@/features/auth/@types/session-status-enum";
2
+
3
+ /**
4
+ * Represents the state of a user session and associated actions.
5
+ */
6
+ export interface SessionState {
7
+ /**
8
+ * The current status of the session.
9
+ */
10
+ status: SessionStatus;
11
+
12
+ /**
13
+ * Logs in the user with the provided parameters.
14
+ * @param {LoginParamsEntity} params - The parameters required to log in the user.
15
+ */
16
+ login: () => Promise<void>;
17
+
18
+ /**
19
+ * Logs out the current user.
20
+ */
21
+ logout: () => void;
22
+
23
+ /**
24
+ * Attempts to silently log in the user without their intervention.
25
+ */
26
+ silentLogin: () => Promise<void>;
27
+
28
+ /**
29
+ * Checks if the user is currently authenticated.
30
+ * @returns {boolean} `true` if the user is authenticated, otherwise `false`.
31
+ */
32
+ isAuthenticated: () => boolean;
33
+
34
+ /**
35
+ * Indicates whether the session state has been initialized.
36
+ */
37
+ isInitialized: boolean;
38
+ }
@@ -0,0 +1,16 @@
1
+ export enum SessionStatus {
2
+ /**
3
+ * The user is currently being authenticated or logged out.
4
+ */
5
+ LOADING,
6
+
7
+ /**
8
+ * The user is authenticated.
9
+ */
10
+ AUTHENTICATED,
11
+
12
+ /**
13
+ * The user is not authenticated.
14
+ */
15
+ UNAUTHENTICATED,
16
+ }
@@ -0,0 +1,18 @@
1
+ import { create } from "zustand";
2
+ import { SessionStatus } from "@/features/auth/@types/session-status-enum";
3
+ import { SessionState } from "@/features/auth/@types/session-state";
4
+
5
+ export const useSession = create<SessionState>((set, get) => ({
6
+ status: SessionStatus.UNAUTHENTICATED,
7
+ login: async () => {
8
+ set({ status: SessionStatus.AUTHENTICATED });
9
+ },
10
+ logout: async () => {
11
+ set({ status: SessionStatus.UNAUTHENTICATED });
12
+ },
13
+ silentLogin: async () => {
14
+ set({ status: SessionStatus.AUTHENTICATED });
15
+ },
16
+ isAuthenticated: () => get().status === SessionStatus.AUTHENTICATED,
17
+ isInitialized: false,
18
+ }));
@@ -0,0 +1,25 @@
1
+ import { injectable, inject } from "inversify";
2
+
3
+ import { LocalStorageService } from "@/core/services/local-storage-service";
4
+ import { STORAGE_KEYS } from "@/core/constants/storage-keys";
5
+ import { FavoritesDatasource } from "@/features/favorites/data/datasources/favorites-datasource";
6
+
7
+ @injectable()
8
+ export class FavoritesDatasourceImpl implements FavoritesDatasource {
9
+ constructor(
10
+ @inject(LocalStorageService)
11
+ private readonly localStorageService: LocalStorageService
12
+ ) {}
13
+
14
+ async getFavorites(): Promise<string> {
15
+ return await this.localStorageService.getItem(STORAGE_KEYS.FAVORITES, "[]");
16
+ }
17
+
18
+ async setFavorites(favorites: string): Promise<void> {
19
+ await this.localStorageService.setItem(STORAGE_KEYS.FAVORITES, favorites);
20
+ }
21
+
22
+ async clearFavorites(): Promise<void> {
23
+ await this.localStorageService.removeItem(STORAGE_KEYS.FAVORITES);
24
+ }
25
+ }
@@ -0,0 +1,5 @@
1
+ export abstract class FavoritesDatasource {
2
+ abstract setFavorites(favorites: string): Promise<void>;
3
+ abstract getFavorites(): Promise<string>;
4
+ abstract clearFavorites(): Promise<void>;
5
+ }
@@ -0,0 +1,46 @@
1
+ import { inject, injectable } from "inversify";
2
+ import { FavoritesRepository } from "@/features/favorites/domain/repositories/favorites-repository";
3
+ import { MovieEntity } from "@/features/movies/domain/entities/movie-entity";
4
+ import { FavoritesDatasource } from "@/features/favorites/data/datasources/favorites-datasource";
5
+
6
+ @injectable()
7
+ export class FavoritesRepositoryImpl implements FavoritesRepository {
8
+ constructor(
9
+ @inject(FavoritesDatasource)
10
+ private readonly favoritesDatasource: FavoritesDatasource
11
+ ) {}
12
+
13
+ async addFavorite(movie: MovieEntity): Promise<void> {
14
+ const currentFavorites = await this.favoritesDatasource.getFavorites();
15
+
16
+ const newFavorites = [...JSON.parse(currentFavorites), movie];
17
+
18
+ await this.favoritesDatasource.setFavorites(JSON.stringify(newFavorites));
19
+ }
20
+
21
+ async removeFavorite(movieId: string): Promise<void> {
22
+ const currentFavorites = await this.favoritesDatasource.getFavorites();
23
+
24
+ const favorites = JSON.parse(currentFavorites);
25
+
26
+ const newFavorites = favorites.filter((movie: MovieEntity) => {
27
+ const parsedMovie = { ...movie };
28
+
29
+ return parsedMovie.id !== movieId;
30
+ });
31
+
32
+ await this.favoritesDatasource.setFavorites(JSON.stringify(newFavorites));
33
+ }
34
+
35
+ async getFavorites(): Promise<MovieEntity[]> {
36
+ const currentFavorites = await this.favoritesDatasource.getFavorites();
37
+
38
+ const favorites = JSON.parse(currentFavorites);
39
+
40
+ return favorites;
41
+ }
42
+
43
+ async clearFavorites(): Promise<void> {
44
+ await this.favoritesDatasource.clearFavorites();
45
+ }
46
+ }
@@ -0,0 +1,8 @@
1
+ import { MovieEntity } from "@/features/movies/domain/entities/movie-entity";
2
+
3
+ export abstract class FavoritesRepository {
4
+ abstract getFavorites(): Promise<MovieEntity[]>;
5
+ abstract addFavorite(movie: MovieEntity): Promise<void>;
6
+ abstract removeFavorite(movieId: string): Promise<void>;
7
+ abstract clearFavorites(): Promise<void>;
8
+ }
@@ -0,0 +1,23 @@
1
+ import { inject, injectable } from "inversify";
2
+
3
+ import { Usecase } from "@/core/helpers/usecase";
4
+ import { Result } from "@/core/helpers/result";
5
+ import { FavoritesRepository } from "@/features/favorites/domain/repositories/favorites-repository";
6
+ import { MovieEntity } from "@/features/movies/domain/entities/movie-entity";
7
+ @injectable()
8
+ export class AddToFavoritesUsecase implements Usecase<MovieEntity, void> {
9
+ constructor(
10
+ @inject(FavoritesRepository)
11
+ private readonly favoritesRepository: FavoritesRepository
12
+ ) {}
13
+
14
+ async execute(movie: MovieEntity): Promise<Result<void>> {
15
+ try {
16
+ await this.favoritesRepository.addFavorite(movie);
17
+
18
+ return Result.success(null);
19
+ } catch (error) {
20
+ return Result.failure(error);
21
+ }
22
+ }
23
+ }
@@ -0,0 +1,23 @@
1
+ import { inject, injectable } from "inversify";
2
+
3
+ import { Usecase } from "@/core/helpers/usecase";
4
+ import { Result } from "@/core/helpers/result";
5
+ import { FavoritesRepository } from "@/features/favorites/domain/repositories/favorites-repository";
6
+
7
+ @injectable()
8
+ export class ClearFavoritesUsecase implements Usecase<void, void> {
9
+ constructor(
10
+ @inject(FavoritesRepository)
11
+ private readonly favoritesRepository: FavoritesRepository
12
+ ) {}
13
+
14
+ async execute(): Promise<Result<void>> {
15
+ try {
16
+ await this.favoritesRepository.clearFavorites();
17
+
18
+ return Result.success(null);
19
+ } catch (error) {
20
+ return Result.failure(error);
21
+ }
22
+ }
23
+ }
@@ -0,0 +1,24 @@
1
+ import { inject, injectable } from "inversify";
2
+
3
+ import { Result } from "@/core/helpers/result";
4
+ import { Usecase } from "@/core/helpers/usecase";
5
+ import { FavoritesRepository } from "@/features/favorites/domain/repositories/favorites-repository";
6
+ import { MovieEntity } from "@/features/movies/domain/entities/movie-entity";
7
+
8
+ @injectable()
9
+ export class GetFavoritesUsecase implements Usecase<void, MovieEntity[]> {
10
+ constructor(
11
+ @inject(FavoritesRepository)
12
+ private readonly favoritesRepository: FavoritesRepository
13
+ ) {}
14
+
15
+ async execute(): Promise<Result<MovieEntity[]>> {
16
+ try {
17
+ const favorites = await this.favoritesRepository.getFavorites();
18
+
19
+ return Result.success(favorites);
20
+ } catch (error) {
21
+ return Result.failure(error);
22
+ }
23
+ }
24
+ }
@@ -0,0 +1,23 @@
1
+ import { inject, injectable } from "inversify";
2
+
3
+ import { Usecase } from "@/core/helpers/usecase";
4
+ import { Result } from "@/core/helpers/result";
5
+ import { FavoritesRepository } from "@/features/favorites/domain/repositories/favorites-repository";
6
+
7
+ @injectable()
8
+ export class RemoveFromFavoritesUsecase implements Usecase<string, void> {
9
+ constructor(
10
+ @inject(FavoritesRepository)
11
+ private readonly favoritesRepository: FavoritesRepository
12
+ ) {}
13
+
14
+ async execute(movieId: string): Promise<Result<void>> {
15
+ try {
16
+ await this.favoritesRepository.removeFavorite(movieId);
17
+
18
+ return Result.success(null);
19
+ } catch (error) {
20
+ return Result.failure(error);
21
+ }
22
+ }
23
+ }