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,77 @@
1
+ import {
2
+ View,
3
+ Text,
4
+ StyleSheet,
5
+ Image,
6
+ Touchable,
7
+ TouchableOpacity,
8
+ } from "react-native";
9
+ import { MovieEntity } from "@/features/movies/domain/entities/movie-entity";
10
+ import { useAppStyles } from "@/core/design/hooks/use-app-styles";
11
+ import { AppRadius, AppSpacing } from "@/core/design/theme/app-sizes";
12
+ import { ColorScheme } from "@/core/design/@types/color-scheme";
13
+ import { IMAGE_CONSTANTS } from "@/core/constants/image-constants";
14
+ import { AppSeparator } from "@/core/design/components/app-separator";
15
+ import { AppFontSize, AppFontWeight } from "@/core/design/theme/app-fonts";
16
+ import { router } from "expo-router";
17
+ type Props = {
18
+ movie: MovieEntity;
19
+ };
20
+
21
+ export function FavoritesCard({ movie }: Props) {
22
+ const styles = useAppStyles(createStyles);
23
+
24
+ const redirectToMovieDetails = () => {
25
+ router.push({
26
+ pathname: "/details",
27
+ params: { movieId: movie.id },
28
+ });
29
+ };
30
+
31
+ return (
32
+ <TouchableOpacity style={styles.container} onPress={redirectToMovieDetails}>
33
+ {!!movie.image && (
34
+ <Image
35
+ source={{ uri: movie.image }}
36
+ style={styles.image}
37
+ resizeMode="cover"
38
+ />
39
+ )}
40
+ <View style={styles.content}>
41
+ <Text style={styles.title}>{movie.title}</Text>
42
+ <AppSeparator size={AppSpacing.small} />
43
+ <Text numberOfLines={3} ellipsizeMode="tail" style={styles.description}>
44
+ {movie.description}
45
+ </Text>
46
+ </View>
47
+ </TouchableOpacity>
48
+ );
49
+ }
50
+
51
+ function createStyles(colorScheme: ColorScheme) {
52
+ return StyleSheet.create({
53
+ container: {
54
+ flex: 1,
55
+ backgroundColor: colorScheme.background,
56
+ overflow: "hidden",
57
+ borderRadius: AppRadius.rounded,
58
+ flexDirection: "row",
59
+ },
60
+ content: {
61
+ flex: 1,
62
+ padding: AppSpacing.regular,
63
+ },
64
+ image: {
65
+ height: 100,
66
+ aspectRatio: IMAGE_CONSTANTS.posterAspectRatio,
67
+ },
68
+ title: {
69
+ fontSize: AppFontSize.large,
70
+ fontWeight: AppFontWeight.semibold,
71
+ },
72
+ description: {
73
+ fontSize: AppFontSize.small,
74
+ color: colorScheme.textSecondary,
75
+ },
76
+ });
77
+ }
@@ -0,0 +1,24 @@
1
+ import { useMutation, useQueryClient } from "@tanstack/react-query";
2
+
3
+ import { QUERY_KEYS } from "@/core/constants/query-keys";
4
+ import container from "@/core/di/injection-container";
5
+ import { AddToFavoritesUsecase } from "@/features/favorites/domain/usecases/add-to-favorites-usecase";
6
+ import { MovieEntity } from "@/features/movies/domain/entities/movie-entity";
7
+
8
+ export function useAddFavorite() {
9
+ const queryClient = useQueryClient();
10
+ const addToFavoritesUsecase = container.get(AddToFavoritesUsecase);
11
+
12
+ return useMutation({
13
+ mutationFn: async (movie: MovieEntity) => {
14
+ const result = await addToFavoritesUsecase.execute(movie);
15
+
16
+ if (result.isFailure) {
17
+ throw result.error;
18
+ }
19
+ },
20
+ onSuccess: () => {
21
+ queryClient.invalidateQueries({ queryKey: [QUERY_KEYS.FAVORITES] });
22
+ },
23
+ });
24
+ }
@@ -0,0 +1,22 @@
1
+ import container from "@/core/di/injection-container";
2
+ import { ClearFavoritesUsecase } from "../../domain/usecases/clear-favorites-usecase";
3
+ import { useMutation, useQueryClient } from "@tanstack/react-query";
4
+ import { QUERY_KEYS } from "@/core/constants/query-keys";
5
+
6
+ export function useClearFavorites() {
7
+ const queryClient = useQueryClient();
8
+ const clearFavoritesUsecase = container.get(ClearFavoritesUsecase);
9
+
10
+ return useMutation({
11
+ mutationFn: async () => {
12
+ const result = await clearFavoritesUsecase.execute();
13
+
14
+ if (result.isFailure) {
15
+ throw result.error;
16
+ }
17
+ },
18
+ onSuccess: () => {
19
+ queryClient.invalidateQueries({ queryKey: [QUERY_KEYS.FAVORITES] });
20
+ },
21
+ });
22
+ }
@@ -0,0 +1,22 @@
1
+ import { useQuery } from "@tanstack/react-query";
2
+
3
+ import { QUERY_KEYS } from "@/core/constants/query-keys";
4
+ import container from "@/core/di/injection-container";
5
+ import { GetFavoritesUsecase } from "@/features/favorites/domain/usecases/get-favorites-usecase";
6
+
7
+ export function useFavorites() {
8
+ const getFavoritesUsecase = container.get(GetFavoritesUsecase);
9
+
10
+ return useQuery({
11
+ queryKey: [QUERY_KEYS.FAVORITES],
12
+ queryFn: async () => {
13
+ const result = await getFavoritesUsecase.execute();
14
+
15
+ if (result.isFailure) {
16
+ throw result.error;
17
+ }
18
+
19
+ return result.data;
20
+ },
21
+ });
22
+ }
@@ -0,0 +1,24 @@
1
+ import { useMutation, useQueryClient } from "@tanstack/react-query";
2
+
3
+ import { QUERY_KEYS } from "@/core/constants/query-keys";
4
+ import container from "@/core/di/injection-container";
5
+ import { RemoveFromFavoritesUsecase } from "@/features/favorites/domain/usecases/remove-from-favorites-usecase";
6
+
7
+ export function useRemoveFavorite() {
8
+ const queryClient = useQueryClient();
9
+
10
+ const removeFromFavoritesUsecase = container.get(RemoveFromFavoritesUsecase);
11
+
12
+ return useMutation({
13
+ mutationFn: async (movieId: string) => {
14
+ const result = await removeFromFavoritesUsecase.execute(movieId);
15
+
16
+ if (result.isFailure) {
17
+ throw result.error;
18
+ }
19
+ },
20
+ onSuccess: () => {
21
+ queryClient.invalidateQueries({ queryKey: [QUERY_KEYS.FAVORITES] });
22
+ },
23
+ });
24
+ }
@@ -0,0 +1,50 @@
1
+ import { API_CONSTANTS } from "@/core/constants/api-constants";
2
+ import { RestClient } from "@/core/helpers/rest-client";
3
+ import { MovieDetailsResponseModel } from "@/features/movies/data/models/movie-details-model";
4
+ import { MovieModel } from "@/features/movies/data/models/movie-model";
5
+ import { TMDBResponseModel } from "@/features/movies/data/models/tmdb-response-model";
6
+ import { MoviesDatasource } from "@/features/movies/data/datasources/movies-datasource";
7
+
8
+ export class MoviesDatasourceImpl
9
+ extends RestClient
10
+ implements MoviesDatasource
11
+ {
12
+ constructor() {
13
+ super({
14
+ baseUrl: API_CONSTANTS.baseUrl,
15
+ headers: {
16
+ Authorization: `Bearer ${API_CONSTANTS.token}`,
17
+ },
18
+ });
19
+ }
20
+
21
+ async searchMovies(query: string): Promise<MovieModel[]> {
22
+ const response = await this.get<TMDBResponseModel<MovieModel>>({
23
+ path: API_CONSTANTS.endpoints.search,
24
+ queryParameters: { query },
25
+ });
26
+
27
+ return response.results;
28
+ }
29
+
30
+ async getMovieDetails(id: string): Promise<MovieDetailsResponseModel> {
31
+ const response = await this.get<MovieDetailsResponseModel>({
32
+ path: API_CONSTANTS.endpoints.movieDetails(id),
33
+ });
34
+
35
+ return response;
36
+ }
37
+
38
+ async discoverMovies(): Promise<MovieModel[]> {
39
+ const response = await this.get<TMDBResponseModel<MovieModel>>({
40
+ path: API_CONSTANTS.endpoints.discover,
41
+ queryParameters: {
42
+ include_adult: false,
43
+ include_video: false,
44
+ page: 1,
45
+ },
46
+ });
47
+
48
+ return response.results;
49
+ }
50
+ }
@@ -0,0 +1,8 @@
1
+ import { MovieDetailsResponseModel } from "@/features/movies/data/models/movie-details-model";
2
+ import { MovieModel } from "@/features/movies/data/models/movie-model";
3
+
4
+ export abstract class MoviesDatasource {
5
+ abstract searchMovies(query: string): Promise<MovieModel[]>;
6
+ abstract discoverMovies(): Promise<MovieModel[]>;
7
+ abstract getMovieDetails(id: string): Promise<MovieDetailsResponseModel>;
8
+ }
@@ -0,0 +1,67 @@
1
+ import { API_CONSTANTS } from "@/core/constants/api-constants";
2
+ import { MovieDetailsEntity } from "@/features/movies/domain/entities/movie-details-entity";
3
+
4
+ export interface MovieDetailsResponseModel {
5
+ adult: boolean;
6
+ backdrop_path: string;
7
+ belongs_to_collection: null;
8
+ budget: number;
9
+ genres: Array<{
10
+ id: number;
11
+ name: string;
12
+ }>;
13
+ homepage: string;
14
+ id: number;
15
+ imdb_id: string;
16
+ origin_country: string[];
17
+ original_language: string;
18
+ original_title: string;
19
+ overview: string;
20
+ popularity: number;
21
+ poster_path: string;
22
+ production_companies: Array<{
23
+ id: number;
24
+ logo_path: string;
25
+ name: string;
26
+ origin_country: string;
27
+ }>;
28
+ production_countries: Array<{
29
+ iso_3166_1: string;
30
+ name: string;
31
+ }>;
32
+ release_date: string;
33
+ revenue: number;
34
+ runtime: number;
35
+ spoken_languages: Array<{
36
+ english_name: string;
37
+ iso_639_1: string;
38
+ name: string;
39
+ }>;
40
+ status: string;
41
+ tagline: string;
42
+ title: string;
43
+ video: boolean;
44
+ vote_average: number;
45
+ vote_count: number;
46
+ }
47
+
48
+ export const movieDetailsResponseModelToEntity = (
49
+ model: MovieDetailsResponseModel
50
+ ): MovieDetailsEntity => {
51
+ return {
52
+ id: model.id.toString(),
53
+ title: model.title,
54
+ overview: model.overview,
55
+ poster_path: model.poster_path
56
+ ? `${API_CONSTANTS.movieImageBaseUrl}${model.poster_path}`
57
+ : null,
58
+ backdrop_path: model.backdrop_path
59
+ ? `${API_CONSTANTS.movieImageBaseUrl}${model.backdrop_path}`
60
+ : null,
61
+ release_date: model.release_date,
62
+ runtime: model.runtime,
63
+ vote_average: model.vote_average,
64
+ genres: model.genres,
65
+ tagline: model.tagline,
66
+ };
67
+ };
@@ -0,0 +1,30 @@
1
+ import { API_CONSTANTS } from "@/core/constants/api-constants";
2
+ import { MovieEntity } from "@/features/movies/domain/entities/movie-entity";
3
+
4
+ export interface MovieModel {
5
+ adult: boolean;
6
+ backdrop_path: string;
7
+ genre_ids: number[];
8
+ id: number;
9
+ original_language: string;
10
+ original_title: string;
11
+ overview: string;
12
+ popularity: number;
13
+ poster_path: string;
14
+ release_date: string;
15
+ title: string;
16
+ video: boolean;
17
+ vote_average: number;
18
+ vote_count: number;
19
+ }
20
+
21
+ export function movieModelToEntity(movie: MovieModel): MovieEntity {
22
+ return {
23
+ id: movie.id.toString(),
24
+ title: movie.title,
25
+ description: movie.overview,
26
+ image: movie.poster_path
27
+ ? `${API_CONSTANTS.movieImageBaseUrl}${movie.poster_path}`
28
+ : null,
29
+ };
30
+ }
@@ -0,0 +1,6 @@
1
+ export interface TMDBResponseModel<T> {
2
+ page: number;
3
+ results: T[];
4
+ total_pages: number;
5
+ total_results: number;
6
+ }
@@ -0,0 +1,34 @@
1
+ import { inject, injectable } from "inversify";
2
+
3
+ import { MoviesDatasource } from "@/features/movies/data/datasources/movies-datasource";
4
+ import { movieModelToEntity } from "@/features/movies/data/models/movie-model";
5
+ import { movieDetailsResponseModelToEntity } from "@/features/movies/data/models/movie-details-model";
6
+ import { MovieDetailsEntity } from "@/features/movies/domain/entities/movie-details-entity";
7
+ import { MovieEntity } from "@/features/movies/domain/entities/movie-entity";
8
+ import { MoviesRepository } from "@/features/movies/domain/repositories/movies-repository";
9
+
10
+ @injectable()
11
+ export class MoviesRepositoryImpl implements MoviesRepository {
12
+ constructor(
13
+ @inject(MoviesDatasource)
14
+ private readonly moviesDatasource: MoviesDatasource
15
+ ) {}
16
+
17
+ async searchMovies(query: string): Promise<MovieEntity[]> {
18
+ const movies = await this.moviesDatasource.searchMovies(query);
19
+
20
+ return movies.map(movieModelToEntity);
21
+ }
22
+
23
+ async getRandomMovies(): Promise<MovieEntity[]> {
24
+ const movies = await this.moviesDatasource.discoverMovies();
25
+
26
+ return movies.map(movieModelToEntity);
27
+ }
28
+
29
+ async getMovieDetails(id: string): Promise<MovieDetailsEntity> {
30
+ const response = await this.moviesDatasource.getMovieDetails(id);
31
+
32
+ return movieDetailsResponseModelToEntity(response);
33
+ }
34
+ }
@@ -0,0 +1,28 @@
1
+ import { MovieEntity } from "@/features/movies/domain/entities/movie-entity";
2
+
3
+ export interface MovieDetailsEntity {
4
+ id: string;
5
+ title: string;
6
+ overview: string;
7
+ poster_path: string | null;
8
+ backdrop_path: string | null;
9
+ release_date: string;
10
+ runtime: number;
11
+ vote_average: number;
12
+ genres: Array<{
13
+ id: number;
14
+ name: string;
15
+ }>;
16
+ tagline: string;
17
+ }
18
+
19
+ export function movieDetailsEntityToMovieEntity(
20
+ movieDetailsEntity: MovieDetailsEntity
21
+ ): MovieEntity {
22
+ return {
23
+ id: movieDetailsEntity.id,
24
+ title: movieDetailsEntity.title,
25
+ description: movieDetailsEntity.overview,
26
+ image: movieDetailsEntity.poster_path,
27
+ };
28
+ }
@@ -0,0 +1,6 @@
1
+ export interface MovieEntity {
2
+ id: string;
3
+ title: string;
4
+ description: string;
5
+ image: string | null;
6
+ }
@@ -0,0 +1,25 @@
1
+ import { MovieEntity } from "@/features/movies/domain/entities/movie-entity";
2
+ import { MovieDetailsEntity } from "@/features/movies/domain/entities/movie-details-entity";
3
+
4
+ export abstract class MoviesRepository {
5
+ /**
6
+ * Search for movies by query
7
+ * @param query - The query to search for
8
+ * @returns A list of movies
9
+ */
10
+ abstract searchMovies(query: string): Promise<MovieEntity[]>;
11
+
12
+ /**
13
+ * Get a list of random movies
14
+ * @returns A list of movies
15
+ */
16
+ abstract getRandomMovies(): Promise<MovieEntity[]>;
17
+
18
+ /**
19
+ * Get the details of a movie
20
+ * @param id - The id of the movie
21
+ * @returns The details of the movie
22
+ */
23
+ abstract getMovieDetails(id: string): Promise<MovieDetailsEntity>;
24
+ }
25
+
@@ -0,0 +1,26 @@
1
+ import { inject, injectable } from "inversify";
2
+
3
+ import { MovieDetailsEntity } from "@/features/movies/domain/entities/movie-details-entity";
4
+ import { MoviesRepository } from "@/features/movies/domain/repositories/movies-repository";
5
+ import { Usecase } from "@/core/helpers/usecase";
6
+ import { Result } from "@/core/helpers/result";
7
+
8
+ @injectable()
9
+ export class GetMovieDetailsUsecase
10
+ implements Usecase<string, MovieDetailsEntity>
11
+ {
12
+ constructor(
13
+ @inject(MoviesRepository)
14
+ private readonly moviesRepository: MoviesRepository
15
+ ) {}
16
+
17
+ async execute(id: string): Promise<Result<MovieDetailsEntity>> {
18
+ try {
19
+ const movieDetails = await this.moviesRepository.getMovieDetails(id);
20
+
21
+ return Result.success(movieDetails);
22
+ } catch (error) {
23
+ return Result.failure(error);
24
+ }
25
+ }
26
+ }
@@ -0,0 +1,24 @@
1
+ import { inject, injectable } from "inversify";
2
+
3
+ import { MovieEntity } from "@/features/movies/domain/entities/movie-entity";
4
+ import { MoviesRepository } from "@/features/movies/domain/repositories/movies-repository";
5
+ import { Result } from "@/core/helpers/result";
6
+ import { Usecase } from "@/core/helpers/usecase";
7
+
8
+ @injectable()
9
+ export class GetRandomMoviesUsecase implements Usecase<void, MovieEntity[]> {
10
+ constructor(
11
+ @inject(MoviesRepository)
12
+ private readonly moviesRepository: MoviesRepository
13
+ ) {}
14
+
15
+ async execute(): Promise<Result<MovieEntity[]>> {
16
+ try {
17
+ const movies = await this.moviesRepository.getRandomMovies();
18
+
19
+ return Result.success(movies);
20
+ } catch (error) {
21
+ return Result.failure(error);
22
+ }
23
+ }
24
+ }
@@ -0,0 +1,23 @@
1
+ import { inject, injectable } from "inversify";
2
+
3
+ import { MoviesRepository } from "@/features/movies/domain/repositories/movies-repository";
4
+ import { MovieEntity } from "@/features/movies/domain/entities/movie-entity";
5
+ import { Usecase } from "@/core/helpers/usecase";
6
+ import { Result } from "@/core/helpers/result";
7
+
8
+ @injectable()
9
+ export class SearchMoviesUsecase implements Usecase<string, MovieEntity[]> {
10
+ constructor(
11
+ @inject(MoviesRepository)
12
+ private readonly moviesRepository: MoviesRepository
13
+ ) {}
14
+
15
+ async execute(query: string): Promise<Result<MovieEntity[]>> {
16
+ try {
17
+ const movies = await this.moviesRepository.searchMovies(query);
18
+ return Result.success(movies);
19
+ } catch (error) {
20
+ return Result.failure(error);
21
+ }
22
+ }
23
+ }
@@ -0,0 +1,69 @@
1
+ import { View, Text, StyleSheet, TouchableOpacity, Image } from "react-native";
2
+ import { router } from "expo-router";
3
+
4
+ import { useAppStyles } from "@/core/design/hooks/use-app-styles";
5
+ import { IMAGE_CONSTANTS } from "@/core/constants/image-constants";
6
+ import { ColorScheme } from "@/core/design/@types/color-scheme";
7
+ import { MovieEntity } from "@/features/movies/domain/entities/movie-entity";
8
+ import { AppFontSize } from "@/core/design/theme/app-fonts";
9
+ import { AppSpacing } from "@/core/design/theme/app-sizes";
10
+ import { AppSeparator } from "@/core/design/components/app-separator";
11
+
12
+ type Props = {
13
+ movie: MovieEntity;
14
+ };
15
+
16
+ export function MovieTile({ movie }: Props) {
17
+ const styles = useAppStyles(createStyles);
18
+
19
+ const redirectToMovieDetails = () => {
20
+ router.push({
21
+ pathname: "/(protected)/details",
22
+ params: { movieId: movie.id },
23
+ });
24
+ };
25
+
26
+ return (
27
+ <TouchableOpacity style={styles.container} onPress={redirectToMovieDetails}>
28
+ {movie.image ? (
29
+ <Image source={{ uri: movie.image }} style={styles.image} />
30
+ ) : (
31
+ <View style={styles.placeholder}>
32
+ <Text style={styles.placeholderTitle}>{movie.title}</Text>
33
+ <AppSeparator size={AppSpacing.small} />
34
+ <Text numberOfLines={3} style={styles.placeholderDescription}>
35
+ {movie.description}
36
+ </Text>
37
+ </View>
38
+ )}
39
+ </TouchableOpacity>
40
+ );
41
+ }
42
+
43
+ const createStyles = (colorScheme: ColorScheme) =>
44
+ StyleSheet.create({
45
+ container: {
46
+ flex: 1,
47
+ aspectRatio: IMAGE_CONSTANTS.posterAspectRatio,
48
+ },
49
+ image: {
50
+ flex: 1,
51
+ },
52
+ placeholder: {
53
+ flex: 1,
54
+ backgroundColor: colorScheme.background,
55
+ justifyContent: "center",
56
+ alignItems: "center",
57
+ },
58
+ placeholderTitle: {
59
+ textAlign: "center",
60
+ fontSize: AppFontSize.regular,
61
+ fontWeight: "bold",
62
+ color: colorScheme.text,
63
+ },
64
+ placeholderDescription: {
65
+ textAlign: "center",
66
+ fontSize: AppFontSize.small,
67
+ color: colorScheme.textSecondary,
68
+ },
69
+ });
@@ -0,0 +1,22 @@
1
+ import { useQuery } from "@tanstack/react-query";
2
+
3
+ import { QUERY_KEYS } from "@/core/constants/query-keys";
4
+ import container from "@/core/di/injection-container";
5
+ import { GetMovieDetailsUsecase } from "@/features/movies/domain/usecases/get-movie-details-usecase";
6
+
7
+ export function useMovieDetails(movieId: string) {
8
+ const getMovieDetailsUsecase = container.get(GetMovieDetailsUsecase);
9
+
10
+ return useQuery({
11
+ queryKey: [QUERY_KEYS.MOVIE_DETAILS, movieId],
12
+ queryFn: async () => {
13
+ const result = await getMovieDetailsUsecase.execute(movieId);
14
+
15
+ if (result.isSuccess) {
16
+ return result.data;
17
+ } else {
18
+ throw result.error;
19
+ }
20
+ },
21
+ });
22
+ }
@@ -0,0 +1,22 @@
1
+ import { useQuery } from "@tanstack/react-query";
2
+
3
+ import { QUERY_KEYS } from "@/core/constants/query-keys";
4
+ import container from "@/core/di/injection-container";
5
+ import { GetRandomMoviesUsecase } from "@/features/movies/domain/usecases/get-random-movies-usecase";
6
+
7
+ export function useRandomMovies() {
8
+ const getRandomMoviesUsecase = container.get(GetRandomMoviesUsecase);
9
+
10
+ return useQuery({
11
+ queryKey: [QUERY_KEYS.RANDOM_MOVIES],
12
+ queryFn: async () => {
13
+ const result = await getRandomMoviesUsecase.execute();
14
+
15
+ if (result.isSuccess) {
16
+ return result.data;
17
+ } else {
18
+ throw result.error;
19
+ }
20
+ },
21
+ });
22
+ }
@@ -0,0 +1,22 @@
1
+ import { useMutation } from "@tanstack/react-query";
2
+
3
+ import { QUERY_KEYS } from "@/core/constants/query-keys";
4
+ import container from "@/core/di/injection-container";
5
+ import { SearchMoviesUsecase } from "@/features/movies/domain/usecases/search-movies-usecase";
6
+
7
+ export function useSearchMovies() {
8
+ const searchMoviesUsecase = container.get(SearchMoviesUsecase);
9
+
10
+ return useMutation({
11
+ mutationKey: [QUERY_KEYS.SEARCH_MOVIES],
12
+ mutationFn: async (query: string) => {
13
+ const result = await searchMoviesUsecase.execute(query);
14
+
15
+ if (result.isSuccess) {
16
+ return result.data;
17
+ } else {
18
+ throw result.error;
19
+ }
20
+ },
21
+ });
22
+ }