diff --git a/App.tsx b/App.tsx index 7d4db79..5a81486 100644 --- a/App.tsx +++ b/App.tsx @@ -1,5 +1,4 @@ import React, { useEffect, useState } from 'react'; -import "./global.css"; import { NavigationContainer, LinkingOptions } from '@react-navigation/native'; import { AuthProvider } from './src/contexts/AuthContext'; import { useAuth } from './src/hooks/useAuth'; @@ -10,7 +9,7 @@ import { View, ActivityIndicator, StyleSheet } from 'react-native'; // @types import { AuthStackParamList, AppStackParamList } from './src/@types/navigation'; type RootStackParamList = AuthStackParamList & AppStackParamList; - + const linkingConfig: LinkingOptions = { @@ -21,7 +20,7 @@ const linkingConfig: LinkingOptions = { // AuthNavigator (AuthStackParamList) Register: 'register', Login: 'login', - + // AppNavigator (AppStackParamList) MainTabs: { screens: { @@ -44,7 +43,7 @@ function RootNavigator() { setTimeout(() => { setIsAppLoading(false); // react-native-splash-screen (nativo) o .hide() aqui - }, 1500); + }, 1500); }, []); @@ -55,11 +54,9 @@ function RootNavigator() { // <> (Fragmento) necessário return ( <> - {token == null ? ( - - ) : ( - - )} + + + ); } @@ -67,7 +64,7 @@ function RootNavigator() { export default function App() { // o AuthProvider envolve tudo pois passa diretamente o estado do usuário para todos os componentes dentro return ( - + // após isso vem o de navegação linking={linkingConfig} fallback={ diff --git a/android/app/build.gradle b/android/app/build.gradle index 61ddbf8..f5dea22 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -124,3 +124,5 @@ dependencies { implementation("com.google.firebase:firebase-auth") implementation("com.google.firebase:firebase-firestore") } + +apply from: "../../node_modules/react-native-vector-icons/fonts.gradle" diff --git a/dtw/__init__.py b/dtw/__init__.py new file mode 100644 index 0000000..d88682a --- /dev/null +++ b/dtw/__init__.py @@ -0,0 +1,5 @@ +from .dtw import compute_dtw +from .distance import euclidean_distance, manhattan_distance, weighted_distance +from .normalize import center_by_wrist, scale_normalize, normalize_spatial +from .interpolate import interpolate_to_length +from .types import Landmark, TimeSeriesFrame, DTWResult, DTWConfig diff --git a/dtw/distance.py b/dtw/distance.py new file mode 100644 index 0000000..2a21760 --- /dev/null +++ b/dtw/distance.py @@ -0,0 +1,16 @@ +import numpy as np + + +def euclidean_distance(a: np.ndarray, b: np.ndarray) -> float: + diff = a - b + return float(np.sqrt(np.sum(diff ** 2))) + + +def manhattan_distance(a: np.ndarray, b: np.ndarray) -> float: + return float(np.sum(np.abs(a - b))) + + +def weighted_distance(a: np.ndarray, b: np.ndarray, weights: np.ndarray) -> float: + diff = a - b + weighted = weights[:, np.newaxis] * (diff ** 2) + return float(np.sqrt(np.sum(weighted))) diff --git a/dtw/dtw.py b/dtw/dtw.py new file mode 100644 index 0000000..1af842d --- /dev/null +++ b/dtw/dtw.py @@ -0,0 +1,86 @@ +import numpy as np +from .types import TimeSeriesFrame, DTWResult, DTWConfig +from .distance import euclidean_distance, manhattan_distance, weighted_distance + + +def compute_dtw( + series1: list[TimeSeriesFrame], + series2: list[TimeSeriesFrame], + config: DTWConfig | None = None, +) -> DTWResult: + if config is None: + config = DTWConfig() + + n = len(series1) + m = len(series2) + + if n == 0 or m == 0: + return DTWResult( + distance=float("inf"), + normalized_distance=float("inf"), + path=[], + similarity=0.0, + ) + + def get_distance(a: TimeSeriesFrame, b: TimeSeriesFrame) -> float: + ma = a.to_matrix() + mb = b.to_matrix() + if config.distance_metric == "manhattan": + return manhattan_distance(ma, mb) + elif config.distance_metric == "weighted" and config.weights is not None: + return weighted_distance(ma, mb, config.weights) + return euclidean_distance(ma, mb) + + matrix = np.full((n, m), float("inf")) + matrix[0, 0] = get_distance(series1[0], series2[0]) + + for i in range(1, n): + matrix[i, 0] = matrix[i - 1, 0] + get_distance(series1[i], series2[0]) + for j in range(1, m): + matrix[0, j] = matrix[0, j - 1] + get_distance(series1[0], series2[j]) + + for i in range(1, n): + j_start = max(1, i - config.window_size) if config.window_size else 1 + j_end = min(m - 1, i + config.window_size) if config.window_size else m - 1 + + for j in range(j_start, j_end + 1): + cost = get_distance(series1[i], series2[j]) + matrix[i, j] = cost + min( + matrix[i - 1, j], + matrix[i, j - 1], + matrix[i - 1, j - 1], + ) + + path: list[tuple[int, int]] = [] + i, j = n - 1, m - 1 + path.append((i, j)) + + while i > 0 or j > 0: + if i == 0: + j -= 1 + elif j == 0: + i -= 1 + else: + candidates = [matrix[i - 1, j - 1], matrix[i - 1, j], matrix[i, j - 1]] + min_idx = int(np.argmin(candidates)) + if min_idx == 0: + i -= 1 + j -= 1 + elif min_idx == 1: + i -= 1 + else: + j -= 1 + path.append((i, j)) + + path.reverse() + + distance = float(matrix[n - 1, m - 1]) + normalized_distance = distance / len(path) + similarity = 1.0 / (1.0 + normalized_distance) + + return DTWResult( + distance=distance, + normalized_distance=normalized_distance, + path=path, + similarity=similarity, + ) diff --git a/dtw/interpolate.py b/dtw/interpolate.py new file mode 100644 index 0000000..f4080e2 --- /dev/null +++ b/dtw/interpolate.py @@ -0,0 +1,41 @@ +import math +from .types import Landmark, TimeSeriesFrame + + +def interpolate_to_length( + frames: list[TimeSeriesFrame], target_length: int +) -> list[TimeSeriesFrame]: + if not frames or target_length <= 0: + return [] + if len(frames) == target_length: + return frames + + result: list[TimeSeriesFrame] = [] + ratio = (len(frames) - 1) / (target_length - 1) + + for i in range(target_length): + pos = i * ratio + low = math.floor(pos) + high = min(math.ceil(pos), len(frames) - 1) + t = pos - low + + if low == high: + result.append(frames[low]) + continue + + interpolated = [ + Landmark( + x=a.x + t * (b.x - a.x), + y=a.y + t * (b.y - a.y), + z=a.z + t * (b.z - a.z), + visibility=a.visibility, + ) + for a, b in zip(frames[low].landmarks, frames[high].landmarks) + ] + + timestamp = frames[low].timestamp + t * ( + frames[high].timestamp - frames[low].timestamp + ) + result.append(TimeSeriesFrame(timestamp=timestamp, landmarks=interpolated)) + + return result diff --git a/dtw/normalize.py b/dtw/normalize.py new file mode 100644 index 0000000..b909f28 --- /dev/null +++ b/dtw/normalize.py @@ -0,0 +1,48 @@ +import numpy as np +from .types import Landmark, TimeSeriesFrame + + +def center_by_wrist(landmarks: list[Landmark], wrist_index: int = 0) -> list[Landmark]: + if not landmarks or wrist_index >= len(landmarks): + return landmarks + + wrist = landmarks[wrist_index] + return [ + Landmark( + x=lm.x - wrist.x, + y=lm.y - wrist.y, + z=lm.z - wrist.z, + visibility=lm.visibility, + ) + for lm in landmarks + ] + + +def scale_normalize(landmarks: list[Landmark]) -> list[Landmark]: + if not landmarks: + return landmarks + + coords = np.array([[lm.x, lm.y, lm.z] for lm in landmarks]) + max_dist = float(np.max(np.linalg.norm(coords, axis=1))) + + if max_dist == 0: + return landmarks + + return [ + Landmark( + x=lm.x / max_dist, + y=lm.y / max_dist, + z=lm.z / max_dist, + visibility=lm.visibility, + ) + for lm in landmarks + ] + + +def normalize_spatial(frames: list[TimeSeriesFrame]) -> list[TimeSeriesFrame]: + result = [] + for frame in frames: + centered = center_by_wrist(frame.landmarks) + scaled = scale_normalize(centered) + result.append(TimeSeriesFrame(timestamp=frame.timestamp, landmarks=scaled)) + return result diff --git a/dtw/requirements.txt b/dtw/requirements.txt new file mode 100644 index 0000000..87f90de --- /dev/null +++ b/dtw/requirements.txt @@ -0,0 +1 @@ +numpy>=1.24.0 diff --git a/dtw/types.py b/dtw/types.py new file mode 100644 index 0000000..17fa775 --- /dev/null +++ b/dtw/types.py @@ -0,0 +1,39 @@ +from dataclasses import dataclass, field +from typing import Optional +import numpy as np + + +@dataclass +class Landmark: + x: float + y: float + z: float + visibility: Optional[float] = None + + def to_array(self) -> np.ndarray: + return np.array([self.x, self.y, self.z]) + + +@dataclass +class TimeSeriesFrame: + timestamp: float + landmarks: list[Landmark] + + def to_matrix(self) -> np.ndarray: + return np.array([[lm.x, lm.y, lm.z] for lm in self.landmarks]) + + +@dataclass +class DTWResult: + distance: float + normalized_distance: float + path: list[tuple[int, int]] + similarity: float + + +@dataclass +class DTWConfig: + window_size: Optional[int] = None + distance_metric: str = "euclidean" + weights: Optional[np.ndarray] = None + threshold: Optional[float] = None diff --git a/entrypoint.bat b/entrypoint.bat index 3c30168..ede66b8 100644 --- a/entrypoint.bat +++ b/entrypoint.bat @@ -41,7 +41,7 @@ echo [INFO] Emulador pronto! :: Passo 5: Iniciar o container Docker com o Metro Bundler echo [INFO] Iniciando o container Docker com docker-compose... -docker-compose up -d --build +docker-compose up -d :: Passo 6: Configurar o redirecionamento de porta echo [INFO] Configurando 'adb reverse' para a porta 8081... diff --git a/package-lock.json b/package-lock.json index f4f4bfa..ac6b0bb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -39,6 +39,7 @@ "@testing-library/react-native": "^13.3.3", "@types/jest": "^29.5.14", "@types/react": "^19.1.0", + "@types/react-native-vector-icons": "^6.4.18", "@types/react-test-renderer": "^19.1.0", "eslint": "^8.19.0", "firebase-tools": "^14.25.0", @@ -6270,6 +6271,27 @@ "csstype": "^3.0.2" } }, + "node_modules/@types/react-native": { + "version": "0.70.19", + "resolved": "https://registry.npmjs.org/@types/react-native/-/react-native-0.70.19.tgz", + "integrity": "sha512-c6WbyCgWTBgKKMESj/8b4w+zWcZSsCforson7UdXtXMecG3MxCinYi6ihhrHVPyUrVzORsvEzK8zg32z4pK6Sg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/react": "*" + } + }, + "node_modules/@types/react-native-vector-icons": { + "version": "6.4.18", + "resolved": "https://registry.npmjs.org/@types/react-native-vector-icons/-/react-native-vector-icons-6.4.18.tgz", + "integrity": "sha512-YGlNWb+k5laTBHd7+uZowB9DpIK3SXUneZqAiKQaj1jnJCZM0x71GDim5JCTMi4IFkhc9m8H/Gm28T5BjyivUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/react": "*", + "@types/react-native": "^0.70" + } + }, "node_modules/@types/react-test-renderer": { "version": "19.1.0", "resolved": "https://registry.npmjs.org/@types/react-test-renderer/-/react-test-renderer-19.1.0.tgz", diff --git a/package.json b/package.json index 0727247..7e62c52 100644 --- a/package.json +++ b/package.json @@ -44,6 +44,7 @@ "@testing-library/react-native": "^13.3.3", "@types/jest": "^29.5.14", "@types/react": "^19.1.0", + "@types/react-native-vector-icons": "^6.4.18", "@types/react-test-renderer": "^19.1.0", "eslint": "^8.19.0", "firebase-tools": "^14.25.0", diff --git a/src/navigation/AppNavigator.tsx b/src/navigation/AppNavigator.tsx index ff008ab..adde587 100644 --- a/src/navigation/AppNavigator.tsx +++ b/src/navigation/AppNavigator.tsx @@ -9,7 +9,7 @@ import { useNavigation } from '@react-navigation/native'; import { AppStackParamList, AppTabParamList } from '../@types/navigation'; import NotificationScreen from '../screens/app/NotificationScreen'; -import HomeScreen from '../screens/app/Ranking'; +import HomeScreen from '../screens/app/HomeScreen.tsx'; import EventosScreen from '../screens/app/Ranking'; import PerfilScreen from '../screens/app/PerfilScreen'; @@ -50,12 +50,12 @@ function MainTabsNavigator() { ({ tabBarIcon: ({ focused, color, size }) => { - let iconName = 'home'; + let iconName = 'home'; if (route.name === 'Home') { iconName = focused ? 'home' : 'home-outline'; } else if (route.name === 'NewPost') { iconName = focused ? 'add-circle' : 'add-circle-outline'; - return ; + return ; } else if (route.name === 'Eventos') { iconName = focused ? 'calendar' : 'calendar-outline'; } else if (route.name === 'Perfil') { @@ -63,7 +63,7 @@ function MainTabsNavigator() { } return ; }, - tabBarActiveTintColor: '#6200EE', + tabBarActiveTintColor: '#6200EE', tabBarInactiveTintColor: 'gray', tabBarShowLabel: false, // esconde os nomes (Home, Perfil, etc) })} @@ -74,7 +74,7 @@ function MainTabsNavigator() { options={{ headerTitle: 'Feed', // Título headerTitleAlign: 'center', - headerLeft: () => , + headerLeft: () => , headerRight: () => , // Botão de Notificações }} /> @@ -97,7 +97,7 @@ export default function AppNavigator() { options={{ headerShown: false }} // Esconde o header duplicado do Stack /> diff --git a/src/screens/app/HomeScreen.tsx b/src/screens/app/HomeScreen.tsx new file mode 100644 index 0000000..1ff6111 --- /dev/null +++ b/src/screens/app/HomeScreen.tsx @@ -0,0 +1,178 @@ +import React, { use, useEffect } from 'react'; +import { View, Text, StyleSheet, TouchableOpacity, FlatList, Alert } from 'react-native'; +import { useState } from 'react'; +import { useNavigation } from '@react-navigation/native'; + +const CourseCard = ({ title, progress, onPress }) => { + const isCompleted = progress >= 100; + + return ( + + + {/* 1. Header: Title and Percentage */} + + {title} + {progress}% + + + {/* 2. Progress Bar */} + + + + + {/* 3. Footer: Status Text */} + + {isCompleted ? ( + + Concluído + {/* Simple Unicode Checkmark (No icon library needed) */} + + + ) : ( + Continuar + )} + + + + ); +}; +export default function HomeScreen() { + const [courses, setCourses] = useState([]); + const [loading, setLoading] = useState(true); + const navigation = useNavigation(); + + // Fetch courses from database + useEffect(() => { + fetchCourses(); + }, []); + + + const fetchCourses = async () => { + try { + + const mockCourses = [ + { id: '1', title: 'Alimentos', progress: 100 }, + { id: '2', title: 'Saudações', progress: 75 }, + { id: '3', title: 'Números', progress: 45 }, + ]; + setCourses(mockCourses); + } catch (error) { + console.error('Error fetching courses:', error); + } finally { + setLoading(false); + } + }; + + const handleCardPress = (courseId, title) => { + // Navigate with parameters + //navigation.navigate('CourseDetail', { + // courseId, + // title + //}); + + Alert.alert('Navegar para o curso:', `${title} (ID: ${courseId})`); + }; + return ( + + Módulos + + item.id} + renderItem={({ item }) => ( + handleCardPress(item.id, item.title)} + /> + )} + scrollEnabled={false} + /> + + ); +} + +const styles = StyleSheet.create({ + title: { + fontSize: 32, + fontWeight: 'bold', + color: 'blue', + marginBottom: 20, + }, + screenContainer: { + flex: 1, + backgroundColor: '#fff', + padding: 20, + }, + cardContainer: { + backgroundColor: '#fff', + borderRadius: 16, + borderWidth: 1.5, + borderColor: '#6379F2', // The main blue outline color + paddingVertical: 16, + paddingHorizontal: 20, + marginBottom: 20, // Space between cards + // Optional shadow for depth + shadowColor: "#000", + shadowOffset: { width: 0, height: 2 }, + shadowOpacity: 0.05, + shadowRadius: 4, + elevation: 2, + }, + headerRow: { + flexDirection: 'row', + justifyContent: 'space-between', + marginBottom: 10, + }, + titleText: { + fontSize: 16, + color: '#7A869A', // Grayish text for title + fontWeight: '500', + }, + percentageText: { + fontSize: 16, + color: '#7A869A', + fontWeight: '500', + }, + progressBarTrack: { + height: 8, + backgroundColor: '#F0F2F5', // Light gray background for bar + borderRadius: 4, + marginBottom: 15, + overflow: 'hidden', // Ensures the inner bar stays within rounded corners + }, + progressBarFill: { + height: '100%', + backgroundColor: '#2D4CC8', // The strong blue fill color + borderRadius: 4, + }, + footer: { + alignItems: 'center', + justifyContent: 'center', + }, + continueText: { + fontSize: 16, + fontWeight: 'bold', + color: '#2D4CC8', // Blue text + }, + completedContainer: { + flexDirection: 'row', + alignItems: 'center', + }, + completedText: { + fontSize: 16, + fontWeight: 'bold', + color: '#2D4CC8', // Blue text + }, + checkIcon: { + fontSize: 18, + fontWeight: 'bold', + color: '#00C853', // Green color for the checkmark + marginLeft: 6, + }, +}); diff --git a/src/screens/app/HomeScren.tsx b/src/screens/app/HomeScren.tsx deleted file mode 100644 index dbb2772..0000000 --- a/src/screens/app/HomeScren.tsx +++ /dev/null @@ -1,24 +0,0 @@ -import React from 'react'; -import { View, Text, StyleSheet } from 'react-native'; - - -export default function HomeScreen() { - return ( - - Tela Home (Atividades) - Conteúdo do Feed aqui... - - ); -} - -const styles = StyleSheet.create({ - container: { - flex: 1, - justifyContent: 'center', - alignItems: 'center', - }, - title: { - fontSize: 20, - fontWeight: 'bold', - }, -}); diff --git a/src/screens/app/Ranking.tsx b/src/screens/app/Ranking.tsx index ef05d80..f3cedb5 100644 --- a/src/screens/app/Ranking.tsx +++ b/src/screens/app/Ranking.tsx @@ -1,23 +1,208 @@ import React from 'react'; -import { View, Text, StyleSheet } from 'react-native'; +import { View, Text, Image, StyleSheet, FlatList } from 'react-native'; +import { SafeAreaView } from 'react-native-safe-area-context'; +import MaterialCommunityIcons from 'react-native-vector-icons/MaterialCommunityIcons'; +// 1. Mock Data +const PLAYERS = [ + { id: '1', name: 'Antonio Lima', score: 12597, avatar: 'https://i.pravatar.cc/150?u=1', rank: 1 }, + { id: '2', name: 'Fernanda Araujo', score: 12001, avatar: 'https://i.pravatar.cc/150?u=2', rank: 2 }, + { id: '3', name: 'Valdilene Carvalho', score: 11123, avatar: 'https://i.pravatar.cc/150?u=3', rank: 3 }, + { id: '4', name: 'Antonieta Pereira', score: 10002, avatar: 'https://i.pravatar.cc/150?u=4', rank: 4 }, + { id: '5', name: 'Rafael Pereira', score: 5245, avatar: 'https://i.pravatar.cc/150?u=5', rank: 5 }, + { id: '6', name: 'Hugo Souza', score: 4569, avatar: 'https://i.pravatar.cc/150?u=6', rank: 6 }, + { id: '7', name: 'Fernando Lima', score: 3254, avatar: 'https://i.pravatar.cc/150?u=7', rank: 7 }, + { id: '8', name: 'Rafael Pereira', score: 2688, avatar: 'https://i.pravatar.cc/150?u=8', rank: 8 }, +]; + +// 2. Podium Item Component (Top 3) +const PodiumItem = ({ item, size }) => { + const isFirst = item.rank === 1; + const isSecond = item.rank === 2; + const isThird = item.rank === 3; + + // Dynamic styles based on rank + const barHeight = isFirst ? 140 : isSecond ? 100 : 80; + const barColor = '#7F00FF'; // Purple + const iconName = isFirst ? 'trophy' : 'medal'; -export default function EventosScreen() { return ( - - Ranking + + {/* Avatar Group */} + + + + + + {item.name} + + + + {/* Purple Bar */} + + + {item.rank} + + + {/* Score */} + {item.score} ); +}; + +// 3. List Item Component (Rank 4+) +const ListItem = ({ item }) => ( + + {item.rank} + + {item.name} + {item.score} + +); + +export default function EventosScreen() { + // Sort data so top 3 are extracted correctly + const topThree = [PLAYERS[1], PLAYERS[0], PLAYERS[2]]; // Order: 2nd, 1st, 3rd (Visual layout) + const restOfList = PLAYERS.slice(3); + + return ( + + + Ranking + + + item.id} + renderItem={({ item }) => } + contentContainerStyle={styles.listContent} + // The Header contains the Podium + ListHeaderComponent={() => ( + + {topThree.map((player) => ( + + ))} + + )} + /> + + ); } const styles = StyleSheet.create({ container: { flex: 1, + backgroundColor: '#fff', + }, + header: { + paddingHorizontal: 20, + paddingTop: 10, + }, + headerTitle: { + fontSize: 28, + fontWeight: 'bold', + color: '#2B4CDE', // Blue title + }, + // Podium Styles + podiumWrapper: { + flexDirection: 'row', justifyContent: 'center', + alignItems: 'flex-end', + marginTop: 20, + marginBottom: 40, + paddingHorizontal: 10, + }, + podiumContainer: { alignItems: 'center', + marginHorizontal: 8, + width: 90, + }, + avatarContainer: { + alignItems: 'center', + marginBottom: -15, // Pull avatar down slightly over bar + zIndex: 1, + }, + avatarBorder: { + borderWidth: 3, + borderRadius: 40, + padding: 2, + backgroundColor: '#fff', + }, + podiumAvatar: { + width: 60, + height: 60, + borderRadius: 30, + }, + badge: { + backgroundColor: '#1E3A8A', // Dark blue badge + paddingVertical: 2, + paddingHorizontal: 8, + borderRadius: 10, + marginTop: -10, + maxWidth: 85, + }, + badgeText: { + color: '#fff', + fontSize: 10, + fontWeight: 'bold', + }, + bar: { + width: '100%', + borderTopLeftRadius: 10, + borderTopRightRadius: 10, + borderBottomLeftRadius: 10, // Added rounded bottom for floating effect + borderBottomRightRadius: 10, + alignItems: 'center', + justifyContent: 'center', + paddingTop: 10, + }, + rankNumber: { + color: '#fff', + fontSize: 40, + fontWeight: 'bold', + }, + podiumScore: { + color: '#2B4CDE', + fontWeight: 'bold', + marginTop: 5, + fontSize: 16, + }, + // List Styles + listContent: { + paddingHorizontal: 20, + paddingBottom: 20, + }, + card: { + flexDirection: 'row', + alignItems: 'center', + backgroundColor: '#fff', + borderRadius: 15, + padding: 15, + marginBottom: 10, + borderWidth: 1, + borderColor: '#6C9EFF', // Light blue border + }, + rankText: { + fontSize: 18, + fontWeight: 'bold', + color: '#9CA3AF', + width: 30, + textAlign: 'center', + }, + listAvatar: { + width: 40, + height: 40, + borderRadius: 20, + marginHorizontal: 15, + }, + listName: { + flex: 1, + fontSize: 16, + color: '#4B5563', }, - title: { - fontSize: 20, + listScore: { + fontSize: 16, fontWeight: 'bold', + color: '#5476FF', }, -}); +}); \ No newline at end of file diff --git a/tsconfig.json b/tsconfig.json index 31d05ea..5d99a33 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -9,6 +9,7 @@ "**/Pods" ], "compilerOptions": { + "jsx": "react", "resolveJsonModule": true, "esModuleInterop": true, "allowJs": true,