From d1e7b5e1d2884ded4067bffa69c4af9b2fac30c9 Mon Sep 17 00:00:00 2001 From: Meleshko Dmitriy Date: Sat, 6 Dec 2025 17:34:55 +0300 Subject: [PATCH 01/18] feat: initial setup --- mobile-expo/.gitignore | 41 ++++++++++++++++++++++++++ mobile-expo/App.tsx | 20 +++++++++++++ mobile-expo/app.json | 30 +++++++++++++++++++ mobile-expo/assets/adaptive-icon.png | Bin 0 -> 17547 bytes mobile-expo/assets/favicon.png | Bin 0 -> 1466 bytes mobile-expo/assets/icon.png | Bin 0 -> 22380 bytes mobile-expo/assets/splash-icon.png | Bin 0 -> 17547 bytes mobile-expo/index.ts | 8 +++++ mobile-expo/package.json | 42 +++++++++++++++++++++++++++ mobile-expo/tsconfig.json | 6 ++++ 10 files changed, 147 insertions(+) create mode 100644 mobile-expo/.gitignore create mode 100644 mobile-expo/App.tsx create mode 100644 mobile-expo/app.json create mode 100644 mobile-expo/assets/adaptive-icon.png create mode 100644 mobile-expo/assets/favicon.png create mode 100644 mobile-expo/assets/icon.png create mode 100644 mobile-expo/assets/splash-icon.png create mode 100644 mobile-expo/index.ts create mode 100644 mobile-expo/package.json create mode 100644 mobile-expo/tsconfig.json diff --git a/mobile-expo/.gitignore b/mobile-expo/.gitignore new file mode 100644 index 0000000..d914c32 --- /dev/null +++ b/mobile-expo/.gitignore @@ -0,0 +1,41 @@ +# Learn more https://docs.github.com/en/get-started/getting-started-with-git/ignoring-files + +# dependencies +node_modules/ + +# Expo +.expo/ +dist/ +web-build/ +expo-env.d.ts + +# Native +.kotlin/ +*.orig.* +*.jks +*.p8 +*.p12 +*.key +*.mobileprovision + +# Metro +.metro-health-check* + +# debug +npm-debug.* +yarn-debug.* +yarn-error.* + +# macOS +.DS_Store +*.pem + +# local env files +.env*.local + +# typescript +*.tsbuildinfo + +# generated native folders +/ios +/android diff --git a/mobile-expo/App.tsx b/mobile-expo/App.tsx new file mode 100644 index 0000000..493226b --- /dev/null +++ b/mobile-expo/App.tsx @@ -0,0 +1,20 @@ +import { StatusBar } from 'expo-status-bar'; +import { StyleSheet, Text, View } from 'react-native'; + +export default function App() { + return ( + + Open up App.tsx to start working on your app!! + + + ); +} + +const styles = StyleSheet.create({ + container: { + flex: 1, + backgroundColor: '#fff', + alignItems: 'center', + justifyContent: 'center', + }, +}); diff --git a/mobile-expo/app.json b/mobile-expo/app.json new file mode 100644 index 0000000..62b8dfa --- /dev/null +++ b/mobile-expo/app.json @@ -0,0 +1,30 @@ +{ + "expo": { + "name": "mobile-expo", + "slug": "mobile-expo", + "version": "1.0.0", + "orientation": "portrait", + "icon": "./assets/icon.png", + "userInterfaceStyle": "light", + "newArchEnabled": true, + "splash": { + "image": "./assets/splash-icon.png", + "resizeMode": "contain", + "backgroundColor": "#ffffff" + }, + "ios": { + "supportsTablet": true + }, + "android": { + "adaptiveIcon": { + "foregroundImage": "./assets/adaptive-icon.png", + "backgroundColor": "#ffffff" + }, + "edgeToEdgeEnabled": true, + "predictiveBackGestureEnabled": false + }, + "web": { + "favicon": "./assets/favicon.png" + } + } +} diff --git a/mobile-expo/assets/adaptive-icon.png b/mobile-expo/assets/adaptive-icon.png new file mode 100644 index 0000000000000000000000000000000000000000..03d6f6b6c6727954aec1d8206222769afd178d8d GIT binary patch literal 17547 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 literal 0 HcmV?d00001 diff --git a/mobile-expo/assets/icon.png b/mobile-expo/assets/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..a0b1526fc7b78680fd8d733dbc6113e1af695487 GIT binary patch literal 22380 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 literal 0 HcmV?d00001 diff --git a/mobile-expo/assets/splash-icon.png b/mobile-expo/assets/splash-icon.png new file mode 100644 index 0000000000000000000000000000000000000000..03d6f6b6c6727954aec1d8206222769afd178d8d GIT binary patch literal 17547 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 App); +// It also ensures that whether you load the app in Expo Go or in a native build, +// the environment is set up appropriately +registerRootComponent(App); diff --git a/mobile-expo/package.json b/mobile-expo/package.json new file mode 100644 index 0000000..9d5eadc --- /dev/null +++ b/mobile-expo/package.json @@ -0,0 +1,42 @@ +{ + "name": "mobile-expo", + "version": "1.0.0", + "main": "index.ts", + "scripts": { + "start": "expo start", + "android": "expo start --android", + "ios": "expo start --ios", + "web": "expo start --web" + }, + "dependencies": { + "@gorhom/bottom-sheet": "^5.2.8", + "@gorhom/portal": "^1.0.14", + "@react-native-async-storage/async-storage": "^2.2.0", + "@react-navigation/bottom-tabs": "^7.8.11", + "@react-navigation/core": "^7.13.5", + "@react-navigation/native": "^7.1.24", + "@react-navigation/native-stack": "^7.8.5", + "@tanstack/react-query": "^5.90.12", + "axios": "^1.13.2", + "date-fns": "^4.1.0", + "expo": "~54.0.27", + "expo-status-bar": "~3.0.9", + "react": "19.1.0", + "react-native": "0.81.5", + "react-native-gesture-handler": "~2.28.0", + "react-native-reanimated": "~4.1.1", + "react-native-safe-area-context": "~5.6.0", + "react-native-screens": "~4.16.0", + "react-native-unistyles": "^3.0.19", + "zustand": "^5.0.9" + }, + "devDependencies": { + "@types/react": "~19.1.0", + "@types/react-native": "^0.72.8", + "eslint": "^9.39.1", + "eslint-config-prettier": "^10.1.8", + "prettier": "^3.7.4", + "typescript": "~5.9.2" + }, + "private": true +} diff --git a/mobile-expo/tsconfig.json b/mobile-expo/tsconfig.json new file mode 100644 index 0000000..b9567f6 --- /dev/null +++ b/mobile-expo/tsconfig.json @@ -0,0 +1,6 @@ +{ + "extends": "expo/tsconfig.base", + "compilerOptions": { + "strict": true + } +} From d9f265902a71a3e3fe520de37214e84e9553f01d Mon Sep 17 00:00:00 2001 From: Meleshko Dmitriy Date: Sat, 6 Dec 2025 18:33:07 +0300 Subject: [PATCH 02/18] feat: basic navigation --- mobile-expo/App.tsx | 21 ++------- mobile-expo/package.json | 1 + mobile-expo/src/navigation/AppNavigator.tsx | 10 ++++ .../src/navigation/BottomTabsNavigator.tsx | 35 ++++++++++++++ .../src/navigation/SettingsStackNavigator.tsx | 22 +++++++++ .../src/navigation/TasksStackNavigator.tsx | 47 +++++++++++++++++++ mobile-expo/src/navigation/index.ts | 4 ++ mobile-expo/src/screens/index.ts | 2 + .../screens/settings/SettingsListScreen.tsx | 9 ++++ mobile-expo/src/screens/settings/index.ts | 1 + .../src/screens/tasks/CreateTaskScreen.tsx | 10 ++++ .../src/screens/tasks/EditTaskScreen.tsx | 9 ++++ .../src/screens/tasks/TasksListScreen.tsx | 9 ++++ mobile-expo/src/screens/tasks/index.ts | 3 ++ mobile-expo/src/types/index.ts | 1 + mobile-expo/src/types/navigation.types.ts | 7 +++ 16 files changed, 174 insertions(+), 17 deletions(-) create mode 100644 mobile-expo/src/navigation/AppNavigator.tsx create mode 100644 mobile-expo/src/navigation/BottomTabsNavigator.tsx create mode 100644 mobile-expo/src/navigation/SettingsStackNavigator.tsx create mode 100644 mobile-expo/src/navigation/TasksStackNavigator.tsx create mode 100644 mobile-expo/src/navigation/index.ts create mode 100644 mobile-expo/src/screens/index.ts create mode 100644 mobile-expo/src/screens/settings/SettingsListScreen.tsx create mode 100644 mobile-expo/src/screens/settings/index.ts create mode 100644 mobile-expo/src/screens/tasks/CreateTaskScreen.tsx create mode 100644 mobile-expo/src/screens/tasks/EditTaskScreen.tsx create mode 100644 mobile-expo/src/screens/tasks/TasksListScreen.tsx create mode 100644 mobile-expo/src/screens/tasks/index.ts create mode 100644 mobile-expo/src/types/index.ts create mode 100644 mobile-expo/src/types/navigation.types.ts diff --git a/mobile-expo/App.tsx b/mobile-expo/App.tsx index 493226b..bd78ac0 100644 --- a/mobile-expo/App.tsx +++ b/mobile-expo/App.tsx @@ -1,20 +1,7 @@ -import { StatusBar } from 'expo-status-bar'; -import { StyleSheet, Text, View } from 'react-native'; +// import { StatusBar } from 'expo-status-bar'; + +import { AppNavigator } from './src/navigation'; export default function App() { - return ( - - Open up App.tsx to start working on your app!! - - - ); + return ; } - -const styles = StyleSheet.create({ - container: { - flex: 1, - backgroundColor: '#fff', - alignItems: 'center', - justifyContent: 'center', - }, -}); diff --git a/mobile-expo/package.json b/mobile-expo/package.json index 9d5eadc..84aa6a8 100644 --- a/mobile-expo/package.json +++ b/mobile-expo/package.json @@ -9,6 +9,7 @@ "web": "expo start --web" }, "dependencies": { + "@expo/vector-icons": "^15.0.3", "@gorhom/bottom-sheet": "^5.2.8", "@gorhom/portal": "^1.0.14", "@react-native-async-storage/async-storage": "^2.2.0", diff --git a/mobile-expo/src/navigation/AppNavigator.tsx b/mobile-expo/src/navigation/AppNavigator.tsx new file mode 100644 index 0000000..25ad840 --- /dev/null +++ b/mobile-expo/src/navigation/AppNavigator.tsx @@ -0,0 +1,10 @@ +import { NavigationContainer } from '@react-navigation/native'; +import { BottomTabsNavigator } from './BottomTabsNavigator'; + +export const AppNavigator = () => { + return ( + + + + ); +}; diff --git a/mobile-expo/src/navigation/BottomTabsNavigator.tsx b/mobile-expo/src/navigation/BottomTabsNavigator.tsx new file mode 100644 index 0000000..a0d6bf7 --- /dev/null +++ b/mobile-expo/src/navigation/BottomTabsNavigator.tsx @@ -0,0 +1,35 @@ +import { createBottomTabNavigator } from '@react-navigation/bottom-tabs'; +import { TasksStackNavigator } from './TasksStackNavigator'; +import { SettingsStackNavigator } from './SettingsStackNavigator'; + +export type BottomTabsParamList = { + TasksStack: undefined; + SettingsStack: undefined; +}; + +const Tab = createBottomTabNavigator(); + +export const BottomTabsNavigator = () => { + return ( + + + + + ); +}; diff --git a/mobile-expo/src/navigation/SettingsStackNavigator.tsx b/mobile-expo/src/navigation/SettingsStackNavigator.tsx new file mode 100644 index 0000000..eb8c6d8 --- /dev/null +++ b/mobile-expo/src/navigation/SettingsStackNavigator.tsx @@ -0,0 +1,22 @@ +import { createNativeStackNavigator } from '@react-navigation/native-stack'; +import { SettingsListScreen } from '../screens'; + +export type SettingsStackParamList = { + SettingList: undefined; +}; + +const Stack = createNativeStackNavigator(); + +export const SettingsStackNavigator = () => { + return ( + + + + ); +}; diff --git a/mobile-expo/src/navigation/TasksStackNavigator.tsx b/mobile-expo/src/navigation/TasksStackNavigator.tsx new file mode 100644 index 0000000..47fc5c8 --- /dev/null +++ b/mobile-expo/src/navigation/TasksStackNavigator.tsx @@ -0,0 +1,47 @@ +import { createNativeStackNavigator } from '@react-navigation/native-stack'; +import { Ionicons } from '@expo/vector-icons'; +import { CreateTaskScreen, EditTaskScreen, TasksListScreen } from '../screens'; + +export type TasksStackParamList = { + TasksList: undefined; + CreateTask: undefined; + EditTask: { taskId: string }; +}; + +const Stack = createNativeStackNavigator(); + +export const TasksStackNavigator = () => { + return ( + + ( + {}} + /> + ), + }} + /> + + + + ); +}; diff --git a/mobile-expo/src/navigation/index.ts b/mobile-expo/src/navigation/index.ts new file mode 100644 index 0000000..ed65d98 --- /dev/null +++ b/mobile-expo/src/navigation/index.ts @@ -0,0 +1,4 @@ +export { AppNavigator } from './AppNavigator'; +export { BottomTabsNavigator } from './BottomTabsNavigator'; +export { TasksStackNavigator } from './TasksStackNavigator'; +export { SettingsStackNavigator } from './SettingsStackNavigator'; diff --git a/mobile-expo/src/screens/index.ts b/mobile-expo/src/screens/index.ts new file mode 100644 index 0000000..ef41430 --- /dev/null +++ b/mobile-expo/src/screens/index.ts @@ -0,0 +1,2 @@ +export * from './tasks'; +export * from './settings'; diff --git a/mobile-expo/src/screens/settings/SettingsListScreen.tsx b/mobile-expo/src/screens/settings/SettingsListScreen.tsx new file mode 100644 index 0000000..db87df2 --- /dev/null +++ b/mobile-expo/src/screens/settings/SettingsListScreen.tsx @@ -0,0 +1,9 @@ +import { Text, View } from "react-native" + +export const SettingsListScreen = () => { + return ( + + SettingsListScreen + + ) +} \ No newline at end of file diff --git a/mobile-expo/src/screens/settings/index.ts b/mobile-expo/src/screens/settings/index.ts new file mode 100644 index 0000000..13d24ab --- /dev/null +++ b/mobile-expo/src/screens/settings/index.ts @@ -0,0 +1 @@ +export { SettingsListScreen } from './SettingsListScreen'; diff --git a/mobile-expo/src/screens/tasks/CreateTaskScreen.tsx b/mobile-expo/src/screens/tasks/CreateTaskScreen.tsx new file mode 100644 index 0000000..02f6904 --- /dev/null +++ b/mobile-expo/src/screens/tasks/CreateTaskScreen.tsx @@ -0,0 +1,10 @@ +import { Text, View } from "react-native" + + +export const CreateTaskScreen = () => { + return ( + + CreateTaskScreen + + ) +} \ No newline at end of file diff --git a/mobile-expo/src/screens/tasks/EditTaskScreen.tsx b/mobile-expo/src/screens/tasks/EditTaskScreen.tsx new file mode 100644 index 0000000..2dd44cb --- /dev/null +++ b/mobile-expo/src/screens/tasks/EditTaskScreen.tsx @@ -0,0 +1,9 @@ +import { Text, View } from 'react-native'; + +export const EditTaskScreen = () => { + return ( + + EditTaskScreen + + ); +}; diff --git a/mobile-expo/src/screens/tasks/TasksListScreen.tsx b/mobile-expo/src/screens/tasks/TasksListScreen.tsx new file mode 100644 index 0000000..8d92ad0 --- /dev/null +++ b/mobile-expo/src/screens/tasks/TasksListScreen.tsx @@ -0,0 +1,9 @@ +import { Text, View } from 'react-native'; + +export const TasksListScreen = () => { + return ( + + TasksListScreen + + ); +}; diff --git a/mobile-expo/src/screens/tasks/index.ts b/mobile-expo/src/screens/tasks/index.ts new file mode 100644 index 0000000..5d7c13a --- /dev/null +++ b/mobile-expo/src/screens/tasks/index.ts @@ -0,0 +1,3 @@ +export { CreateTaskScreen } from './CreateTaskScreen'; +export { EditTaskScreen } from './EditTaskScreen'; +export { TasksListScreen } from './TasksListScreen'; diff --git a/mobile-expo/src/types/index.ts b/mobile-expo/src/types/index.ts new file mode 100644 index 0000000..cc94226 --- /dev/null +++ b/mobile-expo/src/types/index.ts @@ -0,0 +1 @@ +export * from './navigation.types'; diff --git a/mobile-expo/src/types/navigation.types.ts b/mobile-expo/src/types/navigation.types.ts new file mode 100644 index 0000000..3899688 --- /dev/null +++ b/mobile-expo/src/types/navigation.types.ts @@ -0,0 +1,7 @@ +import { BottomTabsParamList } from '../navigation/BottomTabsNavigator'; + +declare global { + namespace ReactNavigation { + interface RootParamList extends BottomTabsParamList {} + } +} From 6228a53086fd1847290964c2fc08a4f491e74164 Mon Sep 17 00:00:00 2001 From: Meleshko Dmitriy Date: Sat, 6 Dec 2025 19:37:34 +0300 Subject: [PATCH 03/18] feat: basic get request --- .gitignore | 5 -- mobile-expo/.env | 2 + mobile-expo/.gitignore | 3 - .../src/screens/tasks/TasksListScreen.tsx | 30 +++++++++- mobile-expo/src/services/api/apiConfig.ts | 50 +++++++++++++++++ mobile-expo/src/services/api/index.ts | 0 mobile-expo/src/services/index.ts | 1 + mobile-expo/src/types/index.ts | 1 + mobile-expo/src/types/task.types.ts | 56 +++++++++++++++++++ 9 files changed, 139 insertions(+), 9 deletions(-) create mode 100644 mobile-expo/.env create mode 100644 mobile-expo/src/services/api/apiConfig.ts create mode 100644 mobile-expo/src/services/api/index.ts create mode 100644 mobile-expo/src/services/index.ts create mode 100644 mobile-expo/src/types/task.types.ts diff --git a/.gitignore b/.gitignore index 6c9dcee..4a7b4a6 100644 --- a/.gitignore +++ b/.gitignore @@ -8,11 +8,6 @@ dist/ build/ *.tsbuildinfo -# Environment variables -.env -.env.local -.env.*.local - # IDE .vscode/ .idea/ diff --git a/mobile-expo/.env b/mobile-expo/.env new file mode 100644 index 0000000..d56b45e --- /dev/null +++ b/mobile-expo/.env @@ -0,0 +1,2 @@ +NEST_BASE_URL=http://localhost:4200/api/v1 +EXPRESS_BASE_URL=http://localhost:4300/api/v1 \ No newline at end of file diff --git a/mobile-expo/.gitignore b/mobile-expo/.gitignore index d914c32..2e94515 100644 --- a/mobile-expo/.gitignore +++ b/mobile-expo/.gitignore @@ -30,9 +30,6 @@ yarn-error.* .DS_Store *.pem -# local env files -.env*.local - # typescript *.tsbuildinfo diff --git a/mobile-expo/src/screens/tasks/TasksListScreen.tsx b/mobile-expo/src/screens/tasks/TasksListScreen.tsx index 8d92ad0..359518b 100644 --- a/mobile-expo/src/screens/tasks/TasksListScreen.tsx +++ b/mobile-expo/src/screens/tasks/TasksListScreen.tsx @@ -1,9 +1,37 @@ -import { Text, View } from 'react-native'; +import { useEffect, useState } from 'react'; +import { FlatList, Text, View } from 'react-native'; +import { TTask } from '../../types'; +import { apiConfig } from '../../services/api/apiConfig'; export const TasksListScreen = () => { + const [tasks, setTasks] = useState([]); + + useEffect(() => { + const fetchTasks = async () => { + try { + const response = await apiConfig.get('/tasks'); + setTasks(response.data); + } catch (error) { + console.log('Error fetching', error); + } + }; + + fetchTasks(); + }, []); + return ( TasksListScreen + ( + + {item.title} + {item.status} + + )} + keyExtractor={(tasks) => tasks.id} + /> ); }; diff --git a/mobile-expo/src/services/api/apiConfig.ts b/mobile-expo/src/services/api/apiConfig.ts new file mode 100644 index 0000000..ba09b72 --- /dev/null +++ b/mobile-expo/src/services/api/apiConfig.ts @@ -0,0 +1,50 @@ +import axios from "axios"; +import { Platform } from "react-native"; + +const getBaseUrl = () => { + // Android emulator uses a special IP + if (Platform.OS === "android") { + return "http://10.0.2.2:4200/api/v1"; + } + + // iOS simulator should work with localhost + if (Platform.OS === "ios") { + return "http://localhost:4200/api/v1"; + } + + // For web (if you run through expo web) + return "http://localhost:4200/api/v1"; +}; + +export const BASE_URL = getBaseUrl(); + +export const apiConfig = axios.create({ + baseURL: BASE_URL, + timeout: 10000, + headers: { + "Content-Type": "application/json", + }, +}); + +// apiConfig.interceptors.request.use( +// (config) => { +// return config; +// }, +// (error) => { +// return Promise.reject(error); +// } +// ); + +// apiConfig.interceptors.response.use( +// (response) => response, +// (error) => { +// if (error.response) { +// console.error('API Error:', error.response.data); +// } else if (error.request) { +// console.error('Network Error:', error.request); +// } else { +// console.error('Error:', error.message); +// } +// return Promise.reject(error); +// } +// ); diff --git a/mobile-expo/src/services/api/index.ts b/mobile-expo/src/services/api/index.ts new file mode 100644 index 0000000..e69de29 diff --git a/mobile-expo/src/services/index.ts b/mobile-expo/src/services/index.ts new file mode 100644 index 0000000..b1c13e7 --- /dev/null +++ b/mobile-expo/src/services/index.ts @@ -0,0 +1 @@ +export * from './api'; diff --git a/mobile-expo/src/types/index.ts b/mobile-expo/src/types/index.ts index cc94226..6a608dc 100644 --- a/mobile-expo/src/types/index.ts +++ b/mobile-expo/src/types/index.ts @@ -1 +1,2 @@ export * from './navigation.types'; +export * from './task.types'; diff --git a/mobile-expo/src/types/task.types.ts b/mobile-expo/src/types/task.types.ts new file mode 100644 index 0000000..23a8537 --- /dev/null +++ b/mobile-expo/src/types/task.types.ts @@ -0,0 +1,56 @@ +export const enum EnumTaskTag { + WORK = 'work', + HOME = 'home', + PERSONAL = 'personal', + HEALTH = 'health', + SHOPPING = 'shopping', + FINANCE = 'finance', + EDUCATION = 'education', + FAMILY = 'family', + SOCIAL = 'social', + TRAVEL = 'travel', + FOOD = 'food', + CAR = 'car', + PET = 'pet', + HOBBY = 'hobby', + SPORT = 'sport', +} + +export const enum EnumTaskPriority { + LOW = 'low', + MEDIUM = 'medium', + HIGH = 'high', +} + +export const enum EnumTaskStatus { + TODO = 'to-do', + IN_PROGRESS = 'in-progress', + DONE = 'done', +} + +export type TTask = { + id: string; + title: string; + description: string; + tags: EnumTaskTag[]; + priority: EnumTaskPriority; + status: EnumTaskStatus; + isBlocked: boolean; + createdAt: Date; + updateAt: Date; +}; + + +// export type CreateTaskDto = { +// } + +// export type UpdateTaskDto = { +// } + +export type TGetTasksFilters = { + search?: string; + status?: EnumTaskStatus; + priority?: EnumTaskPriority; + tags?: EnumTaskTag[]; + isBlocked?: boolean; +} \ No newline at end of file From d9e18d82a7cb6b60838b997eb127ee1c72d917e0 Mon Sep 17 00:00:00 2001 From: Meleshko Dmitriy Date: Sat, 13 Dec 2025 14:13:04 +0300 Subject: [PATCH 04/18] feat: tasksApi --- mobile-expo/src/constants/index.ts | 3 + .../src/screens/tasks/TasksListScreen.tsx | 30 +-------- mobile-expo/src/services/api/apiClient.ts | 46 ++++++++++++++ mobile-expo/src/services/api/apiConfig.ts | 50 --------------- mobile-expo/src/services/api/index.ts | 2 + mobile-expo/src/services/api/tasksApi.ts | 62 +++++++++++++++++++ mobile-expo/src/types/task.types.ts | 17 +++-- 7 files changed, 125 insertions(+), 85 deletions(-) create mode 100644 mobile-expo/src/constants/index.ts create mode 100644 mobile-expo/src/services/api/apiClient.ts delete mode 100644 mobile-expo/src/services/api/apiConfig.ts create mode 100644 mobile-expo/src/services/api/tasksApi.ts diff --git a/mobile-expo/src/constants/index.ts b/mobile-expo/src/constants/index.ts new file mode 100644 index 0000000..b7049eb --- /dev/null +++ b/mobile-expo/src/constants/index.ts @@ -0,0 +1,3 @@ +export const CONSTANTS = { + DEFAULT_URL: 'http://localhost:4200/api/v1', +} \ No newline at end of file diff --git a/mobile-expo/src/screens/tasks/TasksListScreen.tsx b/mobile-expo/src/screens/tasks/TasksListScreen.tsx index 359518b..8d92ad0 100644 --- a/mobile-expo/src/screens/tasks/TasksListScreen.tsx +++ b/mobile-expo/src/screens/tasks/TasksListScreen.tsx @@ -1,37 +1,9 @@ -import { useEffect, useState } from 'react'; -import { FlatList, Text, View } from 'react-native'; -import { TTask } from '../../types'; -import { apiConfig } from '../../services/api/apiConfig'; +import { Text, View } from 'react-native'; export const TasksListScreen = () => { - const [tasks, setTasks] = useState([]); - - useEffect(() => { - const fetchTasks = async () => { - try { - const response = await apiConfig.get('/tasks'); - setTasks(response.data); - } catch (error) { - console.log('Error fetching', error); - } - }; - - fetchTasks(); - }, []); - return ( TasksListScreen - ( - - {item.title} - {item.status} - - )} - keyExtractor={(tasks) => tasks.id} - /> ); }; diff --git a/mobile-expo/src/services/api/apiClient.ts b/mobile-expo/src/services/api/apiClient.ts new file mode 100644 index 0000000..75ed796 --- /dev/null +++ b/mobile-expo/src/services/api/apiClient.ts @@ -0,0 +1,46 @@ +import axios from "axios"; +import { Platform } from "react-native"; +import { CONSTANTS } from "../../constants"; + +const getBaseUrl = () => { + const envUrl = process.env.NEST_BASE_URL || CONSTANTS.DEFAULT_URL; + + if (Platform.OS === "android" && envUrl.includes("localhost")) { + return envUrl.replace("localhost", "10.0.2.2"); + } + + return envUrl; +}; + +export const BASE_URL = getBaseUrl(); + +export const apiClient = axios.create({ + baseURL: BASE_URL, + timeout: 10000, + headers: { + "Content-Type": "application/json", + }, +}); + +apiClient.interceptors.request.use( + (config) => { + return config; + }, + (error) => { + return Promise.reject(error); + } +); + +apiClient.interceptors.response.use( + (response) => response, + (error) => { + if (error.response) { + console.error('API Error:', error.response.data); + } else if (error.request) { + console.error('Network Error:', error.request); + } else { + console.error('Error:', error.message); + } + return Promise.reject(error); + } +); diff --git a/mobile-expo/src/services/api/apiConfig.ts b/mobile-expo/src/services/api/apiConfig.ts deleted file mode 100644 index ba09b72..0000000 --- a/mobile-expo/src/services/api/apiConfig.ts +++ /dev/null @@ -1,50 +0,0 @@ -import axios from "axios"; -import { Platform } from "react-native"; - -const getBaseUrl = () => { - // Android emulator uses a special IP - if (Platform.OS === "android") { - return "http://10.0.2.2:4200/api/v1"; - } - - // iOS simulator should work with localhost - if (Platform.OS === "ios") { - return "http://localhost:4200/api/v1"; - } - - // For web (if you run through expo web) - return "http://localhost:4200/api/v1"; -}; - -export const BASE_URL = getBaseUrl(); - -export const apiConfig = axios.create({ - baseURL: BASE_URL, - timeout: 10000, - headers: { - "Content-Type": "application/json", - }, -}); - -// apiConfig.interceptors.request.use( -// (config) => { -// return config; -// }, -// (error) => { -// return Promise.reject(error); -// } -// ); - -// apiConfig.interceptors.response.use( -// (response) => response, -// (error) => { -// if (error.response) { -// console.error('API Error:', error.response.data); -// } else if (error.request) { -// console.error('Network Error:', error.request); -// } else { -// console.error('Error:', error.message); -// } -// return Promise.reject(error); -// } -// ); diff --git a/mobile-expo/src/services/api/index.ts b/mobile-expo/src/services/api/index.ts index e69de29..9cad454 100644 --- a/mobile-expo/src/services/api/index.ts +++ b/mobile-expo/src/services/api/index.ts @@ -0,0 +1,2 @@ +export * from './apiClient'; +export * from './tasksApi'; diff --git a/mobile-expo/src/services/api/tasksApi.ts b/mobile-expo/src/services/api/tasksApi.ts new file mode 100644 index 0000000..ba81cde --- /dev/null +++ b/mobile-expo/src/services/api/tasksApi.ts @@ -0,0 +1,62 @@ +import { + CreateTaskPayload, + TGetTasksFilters, + TTask, + UpdateTaskPayload, +} from '../../types'; +import { apiClient } from './apiClient'; + +const TASKS_ROUTE = '/tasks'; + +const buildQueryParams = (filters?: TGetTasksFilters) => { + if (!filters) return {}; + + const params = {}; + + return params; +}; + +export const tasksApi = { + getTasks: async (filters?: TGetTasksFilters): Promise => { + const params = buildQueryParams(filters); + const response = await apiClient.get(`${TASKS_ROUTE}`, { params }); + + return response.data; + }, + + getTaskById: async (id: TTask['id']): Promise => { + const response = await apiClient.get(`${TASKS_ROUTE}/${id}`); + + return response.data; + }, + + createTask: async (payload: CreateTaskPayload): Promise => { + const response = await apiClient.post(`${TASKS_ROUTE}`, payload); + + return response.data; + }, + + updateTask: async ( + id: TTask['id'], + payload: UpdateTaskPayload + ): Promise => { + const response = await apiClient.patch( + `${TASKS_ROUTE}/${id}`, + payload + ); + + return response.data; + }, + + toggleBlockedTask: async (id: TTask['id']): Promise => { + const response = await apiClient.patch( + `${TASKS_ROUTE}/${id}/toggle-blocked` + ); + + return response.data; + }, + + deleteTask: async (id: TTask['id']): Promise => { + await apiClient.delete(`${TASKS_ROUTE}/${id}`); + }, +}; diff --git a/mobile-expo/src/types/task.types.ts b/mobile-expo/src/types/task.types.ts index 23a8537..77276f5 100644 --- a/mobile-expo/src/types/task.types.ts +++ b/mobile-expo/src/types/task.types.ts @@ -40,12 +40,17 @@ export type TTask = { updateAt: Date; }; +export type CreateTaskPayload = Pick< + TTask, + 'title' | 'description' | 'tags' | 'priority' +>; -// export type CreateTaskDto = { -// } - -// export type UpdateTaskDto = { -// } +export type UpdateTaskPayload = Partial< + Pick< + TTask, + 'title' | 'description' | 'tags' | 'priority' | 'status' | 'isBlocked' + > +>; export type TGetTasksFilters = { search?: string; @@ -53,4 +58,4 @@ export type TGetTasksFilters = { priority?: EnumTaskPriority; tags?: EnumTaskTag[]; isBlocked?: boolean; -} \ No newline at end of file +}; From 333e0a210d30ca8a86fe571ed4165688e4fb0e64 Mon Sep 17 00:00:00 2001 From: Meleshko Dmitriy Date: Sat, 13 Dec 2025 14:26:46 +0300 Subject: [PATCH 05/18] feat: tanstack provider --- mobile-expo/App.tsx | 7 ++++- .../src/providers/TanstackProvider.tsx | 26 +++++++++++++++++++ mobile-expo/src/providers/index.ts | 1 + 3 files changed, 33 insertions(+), 1 deletion(-) create mode 100644 mobile-expo/src/providers/TanstackProvider.tsx create mode 100644 mobile-expo/src/providers/index.ts diff --git a/mobile-expo/App.tsx b/mobile-expo/App.tsx index bd78ac0..61d0566 100644 --- a/mobile-expo/App.tsx +++ b/mobile-expo/App.tsx @@ -1,7 +1,12 @@ // import { StatusBar } from 'expo-status-bar'; import { AppNavigator } from './src/navigation'; +import { TanstackProvider } from './src/providers'; export default function App() { - return ; + return ( + + + + ); } diff --git a/mobile-expo/src/providers/TanstackProvider.tsx b/mobile-expo/src/providers/TanstackProvider.tsx new file mode 100644 index 0000000..fae9136 --- /dev/null +++ b/mobile-expo/src/providers/TanstackProvider.tsx @@ -0,0 +1,26 @@ +import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; +import { ReactNode } from 'react'; + +const queryClient = new QueryClient({ + defaultOptions: { + queries: { + retry: 1, + staleTime: 1000 * 60 * 5, // 5 min + refetchOnWindowFocus: false, + refetchOnReconnect: true, + }, + mutations: { + retry: 0, + }, + }, +}); + +interface TanstackProviderProps { + children: ReactNode; +} + +export const TanstackProvider = ({ children }: TanstackProviderProps) => { + return ( + {children} + ); +}; diff --git a/mobile-expo/src/providers/index.ts b/mobile-expo/src/providers/index.ts new file mode 100644 index 0000000..ba6503f --- /dev/null +++ b/mobile-expo/src/providers/index.ts @@ -0,0 +1 @@ +export * from './TanstackProvider'; From b6038723365001d7869ecdfdd7f80966d1b00c18 Mon Sep 17 00:00:00 2001 From: Meleshko Dmitriy Date: Sat, 13 Dec 2025 15:34:21 +0300 Subject: [PATCH 06/18] feat: tasks hooks --- mobile-expo/src/constants/index.ts | 1 + mobile-expo/src/hooks/tasks/index.ts | 101 ++++++++++++++++++ .../src/providers/TanstackProvider.tsx | 3 +- mobile-expo/src/services/api/tasksApi.ts | 9 +- mobile-expo/src/types/task.types.ts | 2 + 5 files changed, 111 insertions(+), 5 deletions(-) create mode 100644 mobile-expo/src/hooks/tasks/index.ts diff --git a/mobile-expo/src/constants/index.ts b/mobile-expo/src/constants/index.ts index b7049eb..b69a70d 100644 --- a/mobile-expo/src/constants/index.ts +++ b/mobile-expo/src/constants/index.ts @@ -1,3 +1,4 @@ export const CONSTANTS = { DEFAULT_URL: 'http://localhost:4200/api/v1', + DEFAULT_STALE_TIME: 1000 * 60 * 5, // 5 min } \ No newline at end of file diff --git a/mobile-expo/src/hooks/tasks/index.ts b/mobile-expo/src/hooks/tasks/index.ts new file mode 100644 index 0000000..d7c1e47 --- /dev/null +++ b/mobile-expo/src/hooks/tasks/index.ts @@ -0,0 +1,101 @@ +import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; +import { + CreateTaskPayload, + TGetTasksFilters, + TIdTask, + TTask, + UpdateTaskPayload, +} from '../../types'; +import { tasksApi } from '../../services'; +import { CONSTANTS } from '../../constants'; + +export const tasksKeysConfig = { + all: ['tasks'], + lists: () => [...tasksKeysConfig.all, 'list'], + list: (filters?: TGetTasksFilters) => [...tasksKeysConfig.lists(), filters], + details: () => [...tasksKeysConfig.all, 'detail'], + detail: (id: TIdTask) => [...tasksKeysConfig.details(), id], +}; + +export const useTasks = (filters?: TGetTasksFilters) => { + return useQuery({ + queryKey: tasksKeysConfig.list(filters), + queryFn: () => tasksApi.getTasks(filters), + staleTime: CONSTANTS.DEFAULT_STALE_TIME, + }); +}; + +export const useTask = (id: TIdTask) => { + return useQuery({ + queryKey: tasksKeysConfig.detail(id), + queryFn: () => tasksApi.getTaskById(id), + enabled: !!id, // only with id + staleTime: CONSTANTS.DEFAULT_STALE_TIME, + }); +}; + +export const useCreateTask = () => { + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: tasksApi.createTask, + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: tasksKeysConfig.lists() }); + }, + onError: (error) => { + console.error('Failed to create task:', error); + }, + }); +}; + +export const useUpdateTask = () => { + const queryClient = useQueryClient(); + + return useMutation< + TTask, + Error, + { + id: TIdTask; + payload: UpdateTaskPayload; + } + >({ + mutationFn: ({ id, payload }) => tasksApi.updateTask(id, payload), + onSuccess: (data, variables) => { + queryClient.setQueryData(tasksKeysConfig.detail(variables.id), data); + queryClient.invalidateQueries({ queryKey: tasksKeysConfig.lists() }); + }, + onError: (error) => { + console.error('Failed to create task:', error); + }, + }); +}; + +export const useToggleTaskBlocked = () => { + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: tasksApi.toggleBlockedTask, + onSuccess: (data, taskId) => { + queryClient.setQueryData(tasksKeysConfig.detail(taskId), data); + queryClient.invalidateQueries({ queryKey: tasksKeysConfig.lists() }); + }, + onError: (error) => { + console.error('Failed to create task:', error); + }, + }); +}; + +export const useDeleteTask = () => { + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: tasksApi.deleteTask, + onSuccess: (_, taskId) => { + queryClient.removeQueries({ queryKey: tasksKeysConfig.detail(taskId) }); + queryClient.invalidateQueries({ queryKey: tasksKeysConfig.lists() }); + }, + onError: (error) => { + console.error('Failed to create task:', error); + }, + }); +}; diff --git a/mobile-expo/src/providers/TanstackProvider.tsx b/mobile-expo/src/providers/TanstackProvider.tsx index fae9136..dde7051 100644 --- a/mobile-expo/src/providers/TanstackProvider.tsx +++ b/mobile-expo/src/providers/TanstackProvider.tsx @@ -1,11 +1,12 @@ import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; import { ReactNode } from 'react'; +import { CONSTANTS } from '../constants'; const queryClient = new QueryClient({ defaultOptions: { queries: { retry: 1, - staleTime: 1000 * 60 * 5, // 5 min + staleTime: CONSTANTS.DEFAULT_STALE_TIME, refetchOnWindowFocus: false, refetchOnReconnect: true, }, diff --git a/mobile-expo/src/services/api/tasksApi.ts b/mobile-expo/src/services/api/tasksApi.ts index ba81cde..e30ba7c 100644 --- a/mobile-expo/src/services/api/tasksApi.ts +++ b/mobile-expo/src/services/api/tasksApi.ts @@ -1,6 +1,7 @@ import { CreateTaskPayload, TGetTasksFilters, + TIdTask, TTask, UpdateTaskPayload, } from '../../types'; @@ -24,7 +25,7 @@ export const tasksApi = { return response.data; }, - getTaskById: async (id: TTask['id']): Promise => { + getTaskById: async (id: TIdTask): Promise => { const response = await apiClient.get(`${TASKS_ROUTE}/${id}`); return response.data; @@ -37,7 +38,7 @@ export const tasksApi = { }, updateTask: async ( - id: TTask['id'], + id: TIdTask, payload: UpdateTaskPayload ): Promise => { const response = await apiClient.patch( @@ -48,7 +49,7 @@ export const tasksApi = { return response.data; }, - toggleBlockedTask: async (id: TTask['id']): Promise => { + toggleBlockedTask: async (id: TIdTask): Promise => { const response = await apiClient.patch( `${TASKS_ROUTE}/${id}/toggle-blocked` ); @@ -56,7 +57,7 @@ export const tasksApi = { return response.data; }, - deleteTask: async (id: TTask['id']): Promise => { + deleteTask: async (id: TIdTask): Promise => { await apiClient.delete(`${TASKS_ROUTE}/${id}`); }, }; diff --git a/mobile-expo/src/types/task.types.ts b/mobile-expo/src/types/task.types.ts index 77276f5..c16812f 100644 --- a/mobile-expo/src/types/task.types.ts +++ b/mobile-expo/src/types/task.types.ts @@ -40,6 +40,8 @@ export type TTask = { updateAt: Date; }; +export type TIdTask = TTask['id']; + export type CreateTaskPayload = Pick< TTask, 'title' | 'description' | 'tags' | 'priority' From 6fa56ea7b72552053abf1d7a5fa2b59264a70add Mon Sep 17 00:00:00 2001 From: Meleshko Dmitriy Date: Sat, 13 Dec 2025 15:41:50 +0300 Subject: [PATCH 07/18] feat: query params --- mobile-expo/src/services/api/tasksApi.ts | 35 ++++++++++++++++++++---- 1 file changed, 30 insertions(+), 5 deletions(-) diff --git a/mobile-expo/src/services/api/tasksApi.ts b/mobile-expo/src/services/api/tasksApi.ts index e30ba7c..5103582 100644 --- a/mobile-expo/src/services/api/tasksApi.ts +++ b/mobile-expo/src/services/api/tasksApi.ts @@ -4,15 +4,40 @@ import { TIdTask, TTask, UpdateTaskPayload, -} from '../../types'; -import { apiClient } from './apiClient'; +} from "../../types"; +import { apiClient } from "./apiClient"; -const TASKS_ROUTE = '/tasks'; +const TASKS_ROUTE = "/tasks"; -const buildQueryParams = (filters?: TGetTasksFilters) => { +const buildQueryParams = ( + filters?: TGetTasksFilters +): Record => { if (!filters) return {}; - const params = {}; + const params: Record = {}; + + if (filters.status) { + params.status = filters.status; + } + + if (filters.priority) { + params.priority = filters.priority; + } + + if (filters.tags && filters.tags.length > 0) { + params.tags = filters.tags.join(","); + } + + if ( + filters.isBlocked !== undefined && + typeof filters.isBlocked === "boolean" + ) { + params.isBlocked = filters.isBlocked; + } + + if (filters.search && filters.search.trim().length > 0) { + params.search = filters.search.trim(); + } return params; }; From f5d6387fbc62b0b3e0cb97490cf915f2926a3922 Mon Sep 17 00:00:00 2001 From: Meleshko Dmitriy Date: Sat, 13 Dec 2025 15:45:39 +0300 Subject: [PATCH 08/18] fix: tasks api --- mobile-expo/src/hooks/index.ts | 2 ++ mobile-expo/src/hooks/tasks/index.ts | 6 +++--- 2 files changed, 5 insertions(+), 3 deletions(-) create mode 100644 mobile-expo/src/hooks/index.ts diff --git a/mobile-expo/src/hooks/index.ts b/mobile-expo/src/hooks/index.ts new file mode 100644 index 0000000..3c78011 --- /dev/null +++ b/mobile-expo/src/hooks/index.ts @@ -0,0 +1,2 @@ +export * from './tasks'; + diff --git a/mobile-expo/src/hooks/tasks/index.ts b/mobile-expo/src/hooks/tasks/index.ts index d7c1e47..1688f8e 100644 --- a/mobile-expo/src/hooks/tasks/index.ts +++ b/mobile-expo/src/hooks/tasks/index.ts @@ -65,7 +65,7 @@ export const useUpdateTask = () => { queryClient.invalidateQueries({ queryKey: tasksKeysConfig.lists() }); }, onError: (error) => { - console.error('Failed to create task:', error); + console.error('Failed to update task:', error); }, }); }; @@ -80,7 +80,7 @@ export const useToggleTaskBlocked = () => { queryClient.invalidateQueries({ queryKey: tasksKeysConfig.lists() }); }, onError: (error) => { - console.error('Failed to create task:', error); + console.error('Failed to toggle task blocked status:', error); }, }); }; @@ -95,7 +95,7 @@ export const useDeleteTask = () => { queryClient.invalidateQueries({ queryKey: tasksKeysConfig.lists() }); }, onError: (error) => { - console.error('Failed to create task:', error); + console.error('Failed to delete task:', error); }, }); }; From 34ed03f40b606a4ba75138b8afec63c39c6a2bf4 Mon Sep 17 00:00:00 2001 From: Meleshko Dmitriy Date: Sun, 14 Dec 2025 11:02:14 +0300 Subject: [PATCH 09/18] feat: screen wrappers & providers --- mobile-expo/App.tsx | 13 +-- mobile-expo/package.json | 2 + .../src/components/common/ErrorBoundary.tsx | 81 ++++++++++++++++++ .../src/components/common/ScreenWrapper.tsx | 84 +++++++++++++++++++ mobile-expo/src/components/common/index.ts | 2 + mobile-expo/src/components/index.ts | 1 + mobile-expo/src/providers/AppProvider.tsx | 17 ++++ mobile-expo/src/providers/index.ts | 1 + .../screens/settings/SettingsListScreen.tsx | 11 +-- .../src/screens/tasks/CreateTaskScreen.tsx | 12 +-- .../src/screens/tasks/EditTaskScreen.tsx | 7 +- .../src/screens/tasks/TasksListScreen.tsx | 25 +++++- 12 files changed, 232 insertions(+), 24 deletions(-) create mode 100644 mobile-expo/src/components/common/ErrorBoundary.tsx create mode 100644 mobile-expo/src/components/common/ScreenWrapper.tsx create mode 100644 mobile-expo/src/components/common/index.ts create mode 100644 mobile-expo/src/components/index.ts create mode 100644 mobile-expo/src/providers/AppProvider.tsx diff --git a/mobile-expo/App.tsx b/mobile-expo/App.tsx index 61d0566..4293eae 100644 --- a/mobile-expo/App.tsx +++ b/mobile-expo/App.tsx @@ -1,12 +1,13 @@ -// import { StatusBar } from 'expo-status-bar'; - +import { ErrorBoundary } from './src/components'; import { AppNavigator } from './src/navigation'; -import { TanstackProvider } from './src/providers'; +import { AppProvider } from './src/providers'; export default function App() { return ( - - - + + + + + ); } diff --git a/mobile-expo/package.json b/mobile-expo/package.json index 84aa6a8..f886be9 100644 --- a/mobile-expo/package.json +++ b/mobile-expo/package.json @@ -23,8 +23,10 @@ "expo": "~54.0.27", "expo-status-bar": "~3.0.9", "react": "19.1.0", + "react-error-boundary": "^6.0.0", "react-native": "0.81.5", "react-native-gesture-handler": "~2.28.0", + "react-native-keyboard-aware-scroll-view": "^0.9.5", "react-native-reanimated": "~4.1.1", "react-native-safe-area-context": "~5.6.0", "react-native-screens": "~4.16.0", diff --git a/mobile-expo/src/components/common/ErrorBoundary.tsx b/mobile-expo/src/components/common/ErrorBoundary.tsx new file mode 100644 index 0000000..fa65987 --- /dev/null +++ b/mobile-expo/src/components/common/ErrorBoundary.tsx @@ -0,0 +1,81 @@ +import { Component, ReactNode } from 'react'; +import { View, Text, StyleSheet, TouchableOpacity } from 'react-native'; + +interface Props { + children: ReactNode; + fallback?: ReactNode; +} + +interface State { + hasError: boolean; + error?: Error; +} + +export class ErrorBoundary extends Component { + constructor(props: Props) { + super(props); + this.state = { hasError: false }; + } + + static getDerivedStateFromError(error: Error): State { + return { hasError: true, error }; + } + + componentDidCatch(error: Error, errorInfo: any) { + console.error('ErrorBoundary caught an error:', error, errorInfo); + } + + handleReset = () => { + this.setState({ hasError: false, error: undefined }); + }; + + render() { + if (this.state.hasError) { + if (this.props.fallback) { + return this.props.fallback; + } + + return ( + + Something went wrong + {this.state.error?.message} + + Try again + + + ); + } + + return this.props.children; + } +} + +const styles = StyleSheet.create({ + container: { + flex: 1, + justifyContent: 'center', + alignItems: 'center', + padding: 20, + }, + title: { + fontSize: 20, + fontWeight: 'bold', + marginBottom: 10, + }, + message: { + fontSize: 14, + color: '#666', + marginBottom: 20, + textAlign: 'center', + }, + button: { + backgroundColor: '#007AFF', + paddingHorizontal: 20, + paddingVertical: 10, + borderRadius: 8, + }, + buttonText: { + color: '#fff', + fontWeight: '600', + }, +}); diff --git a/mobile-expo/src/components/common/ScreenWrapper.tsx b/mobile-expo/src/components/common/ScreenWrapper.tsx new file mode 100644 index 0000000..9347361 --- /dev/null +++ b/mobile-expo/src/components/common/ScreenWrapper.tsx @@ -0,0 +1,84 @@ +// src/components/common/ScreenWrapper.tsx +import { ReactNode } from 'react'; +import { + View, + StyleSheet, + ScrollView, + KeyboardAvoidingView, + Platform, +} from 'react-native'; +import { useSafeAreaInsets } from 'react-native-safe-area-context'; + +interface ScreenWrapperProps { + children: ReactNode; + safeAreaTop?: boolean; + safeAreaBottom?: boolean; + scrollable?: boolean; + keyboardAvoiding?: boolean; + style?: View['props']['style']; + contentStyle?: View['props']['style']; +} + +export const ScreenWrapper = ({ + children, + safeAreaTop = true, + safeAreaBottom = true, + scrollable = false, + keyboardAvoiding = false, + style, + contentStyle, +}: ScreenWrapperProps) => { + const insets = useSafeAreaInsets(); + + const containerStyle = [ + styles.container, + { + paddingTop: safeAreaTop ? insets.top : 0, + paddingBottom: safeAreaBottom ? insets.bottom : 0, + }, + style, + ]; + + const content = scrollable ? ( + + {children} + + ) : ( + {children} + ); + + if (keyboardAvoiding) { + return ( + + {content} + + ); + } + + return {content}; +}; + +const styles = StyleSheet.create({ + container: { + flex: 1, + backgroundColor: '#fff', + }, + content: { + flex: 1, + }, + scrollView: { + flex: 1, + }, + scrollContent: { + flexGrow: 1, + }, +}); diff --git a/mobile-expo/src/components/common/index.ts b/mobile-expo/src/components/common/index.ts new file mode 100644 index 0000000..203bad8 --- /dev/null +++ b/mobile-expo/src/components/common/index.ts @@ -0,0 +1,2 @@ +export * from './ScreenWrapper'; +export * from './ErrorBoundary'; diff --git a/mobile-expo/src/components/index.ts b/mobile-expo/src/components/index.ts new file mode 100644 index 0000000..89a3196 --- /dev/null +++ b/mobile-expo/src/components/index.ts @@ -0,0 +1 @@ +export * from './common' diff --git a/mobile-expo/src/providers/AppProvider.tsx b/mobile-expo/src/providers/AppProvider.tsx new file mode 100644 index 0000000..6476912 --- /dev/null +++ b/mobile-expo/src/providers/AppProvider.tsx @@ -0,0 +1,17 @@ +import { ReactNode } from 'react'; +import { SafeAreaProvider } from 'react-native-safe-area-context'; +import { StatusBar } from 'expo-status-bar'; +import { TanstackProvider } from './TanstackProvider'; + +interface AppProviderProps { + children: ReactNode; +} + +export const AppProvider = ({ children }: AppProviderProps) => { + return ( + + + {children} + + ); +}; diff --git a/mobile-expo/src/providers/index.ts b/mobile-expo/src/providers/index.ts index ba6503f..41444b3 100644 --- a/mobile-expo/src/providers/index.ts +++ b/mobile-expo/src/providers/index.ts @@ -1 +1,2 @@ export * from './TanstackProvider'; +export * from './AppProvider'; diff --git a/mobile-expo/src/screens/settings/SettingsListScreen.tsx b/mobile-expo/src/screens/settings/SettingsListScreen.tsx index db87df2..a455a34 100644 --- a/mobile-expo/src/screens/settings/SettingsListScreen.tsx +++ b/mobile-expo/src/screens/settings/SettingsListScreen.tsx @@ -1,9 +1,10 @@ -import { Text, View } from "react-native" +import { Text } from "react-native"; +import { ScreenWrapper } from "../../components"; export const SettingsListScreen = () => { return ( - + SettingsListScreen - - ) -} \ No newline at end of file + + ); +}; \ No newline at end of file diff --git a/mobile-expo/src/screens/tasks/CreateTaskScreen.tsx b/mobile-expo/src/screens/tasks/CreateTaskScreen.tsx index 02f6904..a8b3142 100644 --- a/mobile-expo/src/screens/tasks/CreateTaskScreen.tsx +++ b/mobile-expo/src/screens/tasks/CreateTaskScreen.tsx @@ -1,10 +1,10 @@ -import { Text, View } from "react-native" - +import { Text } from "react-native"; +import { ScreenWrapper } from "../../components"; export const CreateTaskScreen = () => { return ( - + CreateTaskScreen - - ) -} \ No newline at end of file + + ); +}; \ No newline at end of file diff --git a/mobile-expo/src/screens/tasks/EditTaskScreen.tsx b/mobile-expo/src/screens/tasks/EditTaskScreen.tsx index 2dd44cb..3aed6dc 100644 --- a/mobile-expo/src/screens/tasks/EditTaskScreen.tsx +++ b/mobile-expo/src/screens/tasks/EditTaskScreen.tsx @@ -1,9 +1,10 @@ -import { Text, View } from 'react-native'; +import { Text } from 'react-native'; +import { ScreenWrapper } from '../../components'; export const EditTaskScreen = () => { return ( - + EditTaskScreen - + ); }; diff --git a/mobile-expo/src/screens/tasks/TasksListScreen.tsx b/mobile-expo/src/screens/tasks/TasksListScreen.tsx index 8d92ad0..ef45aee 100644 --- a/mobile-expo/src/screens/tasks/TasksListScreen.tsx +++ b/mobile-expo/src/screens/tasks/TasksListScreen.tsx @@ -1,9 +1,26 @@ -import { Text, View } from 'react-native'; +import { FlatList, Text, View } from 'react-native'; +import { ScreenWrapper } from '../../components'; +import { useTasks } from '../../hooks'; export const TasksListScreen = () => { + const { data: tasks, isLoading } = useTasks(); + + if (isLoading) { + return ( + + Loading.... + + ); + } + return ( - - TasksListScreen - + + TasksListScreenTasksListScreen + task.id} + renderItem={({ item }) => {item.title}} + /> + ); }; From 230ce30440a261dc15a48fbd640679e74cc2530b Mon Sep 17 00:00:00 2001 From: Meleshko Dmitriy Date: Sun, 14 Dec 2025 12:54:47 +0300 Subject: [PATCH 10/18] feat: custom header --- .../components/common/CustomHeader/index.tsx | 51 ++++++++++++++++++ .../CustomHeader/ui/CustomHeaderButton.tsx | 30 +++++++++++ .../src/components/common/ScreenWrapper.tsx | 7 +-- mobile-expo/src/components/common/index.ts | 1 + .../src/navigation/SettingsStackNavigator.tsx | 13 +++-- .../src/navigation/TasksStackNavigator.tsx | 52 +++++++++++++------ .../src/screens/tasks/TasksListScreen.tsx | 12 +++-- 7 files changed, 139 insertions(+), 27 deletions(-) create mode 100644 mobile-expo/src/components/common/CustomHeader/index.tsx create mode 100644 mobile-expo/src/components/common/CustomHeader/ui/CustomHeaderButton.tsx diff --git a/mobile-expo/src/components/common/CustomHeader/index.tsx b/mobile-expo/src/components/common/CustomHeader/index.tsx new file mode 100644 index 0000000..73044de --- /dev/null +++ b/mobile-expo/src/components/common/CustomHeader/index.tsx @@ -0,0 +1,51 @@ +import { View, Text } from 'react-native'; +import { useSafeAreaInsets } from 'react-native-safe-area-context'; +import { + CustomHeaderButton, + TCustomHeaderButton, +} from './ui/CustomHeaderButton'; + +interface CustomHeaderProps { + title?: string; + leftButton?: TCustomHeaderButton; + rightButton?: TCustomHeaderButton; +} + +export const CustomHeader = ({ + title, + leftButton, + rightButton, +}: CustomHeaderProps) => { + const insets = useSafeAreaInsets(); + + return ( + + + + {leftButton && } + + + + {title} + + + + {rightButton && } + + + + ); +}; diff --git a/mobile-expo/src/components/common/CustomHeader/ui/CustomHeaderButton.tsx b/mobile-expo/src/components/common/CustomHeader/ui/CustomHeaderButton.tsx new file mode 100644 index 0000000..1fe0255 --- /dev/null +++ b/mobile-expo/src/components/common/CustomHeader/ui/CustomHeaderButton.tsx @@ -0,0 +1,30 @@ +import { Ionicons } from '@expo/vector-icons'; +import type { ComponentProps } from 'react'; +import { TouchableOpacity } from 'react-native'; + +type IoniconsName = ComponentProps['name']; +export type TCustomHeaderButton = { + name: IoniconsName; + onPress?: () => void; + size?: number; + color?: string; +}; + +interface CustomHeaderButtonProps extends TCustomHeaderButton {} + +export const CustomHeaderButton = ({ + name, + onPress = () => {}, + size = 30, + color = '#000', +}: CustomHeaderButtonProps) => { + return ( + + + + ); +}; diff --git a/mobile-expo/src/components/common/ScreenWrapper.tsx b/mobile-expo/src/components/common/ScreenWrapper.tsx index 9347361..c11bbfc 100644 --- a/mobile-expo/src/components/common/ScreenWrapper.tsx +++ b/mobile-expo/src/components/common/ScreenWrapper.tsx @@ -21,8 +21,8 @@ interface ScreenWrapperProps { export const ScreenWrapper = ({ children, - safeAreaTop = true, - safeAreaBottom = true, + safeAreaTop = false, + safeAreaBottom = false, scrollable = false, keyboardAvoiding = false, style, @@ -70,13 +70,14 @@ export const ScreenWrapper = ({ const styles = StyleSheet.create({ container: { flex: 1, - backgroundColor: '#fff', }, content: { flex: 1, + padding: 16, }, scrollView: { flex: 1, + padding: 16, }, scrollContent: { flexGrow: 1, diff --git a/mobile-expo/src/components/common/index.ts b/mobile-expo/src/components/common/index.ts index 203bad8..3067b44 100644 --- a/mobile-expo/src/components/common/index.ts +++ b/mobile-expo/src/components/common/index.ts @@ -1,2 +1,3 @@ export * from './ScreenWrapper'; export * from './ErrorBoundary'; +export * from './CustomHeader' \ No newline at end of file diff --git a/mobile-expo/src/navigation/SettingsStackNavigator.tsx b/mobile-expo/src/navigation/SettingsStackNavigator.tsx index eb8c6d8..9852ad2 100644 --- a/mobile-expo/src/navigation/SettingsStackNavigator.tsx +++ b/mobile-expo/src/navigation/SettingsStackNavigator.tsx @@ -1,5 +1,6 @@ import { createNativeStackNavigator } from '@react-navigation/native-stack'; import { SettingsListScreen } from '../screens'; +import { CustomHeader } from '../components'; export type SettingsStackParamList = { SettingList: undefined; @@ -9,13 +10,17 @@ const Stack = createNativeStackNavigator(); export const SettingsStackNavigator = () => { return ( - + ({ + header: () => ( + + ), + })} /> ); diff --git a/mobile-expo/src/navigation/TasksStackNavigator.tsx b/mobile-expo/src/navigation/TasksStackNavigator.tsx index 47fc5c8..fea9628 100644 --- a/mobile-expo/src/navigation/TasksStackNavigator.tsx +++ b/mobile-expo/src/navigation/TasksStackNavigator.tsx @@ -1,6 +1,7 @@ import { createNativeStackNavigator } from '@react-navigation/native-stack'; import { Ionicons } from '@expo/vector-icons'; import { CreateTaskScreen, EditTaskScreen, TasksListScreen } from '../screens'; +import { CustomHeader } from '../components'; export type TasksStackParamList = { TasksList: undefined; @@ -12,35 +13,54 @@ const Stack = createNativeStackNavigator(); export const TasksStackNavigator = () => { return ( - + ( - {}} + options={({ navigation }) => ({ + header: () => ( + navigation.navigate("CreateTask"), + }} /> ), - }} + })} /> ({ + header: () => ( + navigation.goBack(), + }} + /> + ), + })} /> ({ + header: () => ( + navigation.goBack(), + }} + /> + ), + })} /> ); diff --git a/mobile-expo/src/screens/tasks/TasksListScreen.tsx b/mobile-expo/src/screens/tasks/TasksListScreen.tsx index ef45aee..3aaabb7 100644 --- a/mobile-expo/src/screens/tasks/TasksListScreen.tsx +++ b/mobile-expo/src/screens/tasks/TasksListScreen.tsx @@ -1,6 +1,6 @@ -import { FlatList, Text, View } from 'react-native'; -import { ScreenWrapper } from '../../components'; -import { useTasks } from '../../hooks'; +import { FlatList, Text, View } from "react-native"; +import { ScreenWrapper } from "../../components"; +import { useTasks } from "../../hooks"; export const TasksListScreen = () => { const { data: tasks, isLoading } = useTasks(); @@ -19,7 +19,11 @@ export const TasksListScreen = () => { task.id} - renderItem={({ item }) => {item.title}} + renderItem={({ item }) => ( + + {item.title} + + )} /> ); From 4b42c26d9245160712b79af8ff2a047308379402 Mon Sep 17 00:00:00 2001 From: Meleshko Dmitriy Date: Sat, 20 Dec 2025 17:06:57 +0300 Subject: [PATCH 11/18] feat: styles --- .../src/components/common/ScreenWrapper.tsx | 1 - .../src/navigation/TasksStackNavigator.tsx | 1 - mobile-expo/src/styles/index.ts | 3 +++ mobile-expo/src/styles/spacing.ts | 10 ++++++++ mobile-expo/src/styles/theme/dark.ts | 23 ++++++++++++++++++ mobile-expo/src/styles/theme/index.ts | 7 ++++++ mobile-expo/src/styles/theme/light.ts | 23 ++++++++++++++++++ mobile-expo/src/styles/typography.ts | 24 +++++++++++++++++++ 8 files changed, 90 insertions(+), 2 deletions(-) create mode 100644 mobile-expo/src/styles/index.ts create mode 100644 mobile-expo/src/styles/spacing.ts create mode 100644 mobile-expo/src/styles/theme/dark.ts create mode 100644 mobile-expo/src/styles/theme/index.ts create mode 100644 mobile-expo/src/styles/theme/light.ts create mode 100644 mobile-expo/src/styles/typography.ts diff --git a/mobile-expo/src/components/common/ScreenWrapper.tsx b/mobile-expo/src/components/common/ScreenWrapper.tsx index c11bbfc..64850f1 100644 --- a/mobile-expo/src/components/common/ScreenWrapper.tsx +++ b/mobile-expo/src/components/common/ScreenWrapper.tsx @@ -1,4 +1,3 @@ -// src/components/common/ScreenWrapper.tsx import { ReactNode } from 'react'; import { View, diff --git a/mobile-expo/src/navigation/TasksStackNavigator.tsx b/mobile-expo/src/navigation/TasksStackNavigator.tsx index fea9628..195d0b7 100644 --- a/mobile-expo/src/navigation/TasksStackNavigator.tsx +++ b/mobile-expo/src/navigation/TasksStackNavigator.tsx @@ -1,5 +1,4 @@ import { createNativeStackNavigator } from '@react-navigation/native-stack'; -import { Ionicons } from '@expo/vector-icons'; import { CreateTaskScreen, EditTaskScreen, TasksListScreen } from '../screens'; import { CustomHeader } from '../components'; diff --git a/mobile-expo/src/styles/index.ts b/mobile-expo/src/styles/index.ts new file mode 100644 index 0000000..35001b5 --- /dev/null +++ b/mobile-expo/src/styles/index.ts @@ -0,0 +1,3 @@ +export * from './theme'; +export * from './spacing'; +export * from './typography'; diff --git a/mobile-expo/src/styles/spacing.ts b/mobile-expo/src/styles/spacing.ts new file mode 100644 index 0000000..d58efbf --- /dev/null +++ b/mobile-expo/src/styles/spacing.ts @@ -0,0 +1,10 @@ +export const spacing = { + xs: 4, + sm: 8, + md: 12, + lg: 20, + xl: 32, + xxl: 48, +} as const; + +export type TSpacing = typeof spacing; \ No newline at end of file diff --git a/mobile-expo/src/styles/theme/dark.ts b/mobile-expo/src/styles/theme/dark.ts new file mode 100644 index 0000000..3376b08 --- /dev/null +++ b/mobile-expo/src/styles/theme/dark.ts @@ -0,0 +1,23 @@ +export const darkTheme = { + colors: { + primary: '#BB86FC', + primaryDark: '#3700B3', + primaryLight: '#6200EE', + background: '#121212', + text: '#FFFFFF', + textSecondary: 'rgba(255, 255, 255, 0.7)', + + priorityLow: '#66BB6A', + priorityMedium: '#FFA726', + priorityHigh: '#EF5350', + + statusTodo: '#757575', + statusInProgress: '#42A5F5', + statusDone: '#66BB6A', + + overlay: 'rgba(0, 0, 0, 0.7)', + backdrop: 'rgba(0, 0, 0, 0.5)', + }, +} as const; + +export type TDarkTheme = typeof darkTheme; \ No newline at end of file diff --git a/mobile-expo/src/styles/theme/index.ts b/mobile-expo/src/styles/theme/index.ts new file mode 100644 index 0000000..d880233 --- /dev/null +++ b/mobile-expo/src/styles/theme/index.ts @@ -0,0 +1,7 @@ +import { TDarkTheme } from './dark'; +import { TLightTheme } from './light'; + +export { lightTheme } from './light'; +export { darkTheme } from './dark'; + +export type TTheme = TLightTheme | TDarkTheme; diff --git a/mobile-expo/src/styles/theme/light.ts b/mobile-expo/src/styles/theme/light.ts new file mode 100644 index 0000000..7ebf670 --- /dev/null +++ b/mobile-expo/src/styles/theme/light.ts @@ -0,0 +1,23 @@ +export const lightTheme = { + colors: { + primary: '#6200EE', + primaryDark: '#3700B3', + primaryLight: '#BB86FC', + background: '#FFFFFF', + text: '#000000', + textSecondary: 'rgba(0, 0, 0, 0.6)', + + priorityLow: '#4CAF50', + priorityMedium: '#FF9800', + priorityHigh: '#F44336', + + statusTodo: '#9E9E9E', + statusInProgress: '#2196F3', + statusDone: '#4CAF50', + + overlay: 'rgba(0, 0, 0, 0.5)', + backdrop: 'rgba(0, 0, 0, 0.32)', + }, +} as const; + +export type TLightTheme = typeof lightTheme; \ No newline at end of file diff --git a/mobile-expo/src/styles/typography.ts b/mobile-expo/src/styles/typography.ts new file mode 100644 index 0000000..9e381ee --- /dev/null +++ b/mobile-expo/src/styles/typography.ts @@ -0,0 +1,24 @@ +export const typography = { + fontSize: { + xs: 12, + sm: 14, + md: 16, + lg: 18, + xl: 20, + xxl: 24, + xxxl: 32, + }, + fontWeight: { + regular: '400' as const, + medium: '500' as const, + semibold: '600' as const, + bold: '700' as const, + }, + lineHeight: { + tight: 1.2, + normal: 1.5, + relaxed: 1.8, + }, +} as const; + +export type TTypography = typeof typography; \ No newline at end of file From b04e85e6a94a1f8e6056b25a90602e2527f50fd7 Mon Sep 17 00:00:00 2001 From: Meleshko Dmitriy Date: Sat, 20 Dec 2025 17:54:31 +0300 Subject: [PATCH 12/18] feat: @ alias --- mobile-expo/babel.config.js | 33 +++++++++++++++++++ mobile-expo/package.json | 1 + mobile-expo/src/hooks/tasks/index.ts | 6 ++-- .../src/navigation/SettingsStackNavigator.tsx | 4 +-- .../src/navigation/TasksStackNavigator.tsx | 4 +-- .../src/providers/TanstackProvider.tsx | 2 +- mobile-expo/src/providers/ThemeProvider.tsx | 2 ++ mobile-expo/src/providers/index.ts | 1 + .../screens/settings/SettingsListScreen.tsx | 2 +- .../src/screens/tasks/CreateTaskScreen.tsx | 2 +- .../src/screens/tasks/EditTaskScreen.tsx | 2 +- .../src/screens/tasks/TasksListScreen.tsx | 4 +-- mobile-expo/src/services/api/apiClient.ts | 2 +- mobile-expo/src/services/api/tasksApi.ts | 2 +- mobile-expo/src/styles/theme/index.ts | 8 ++--- mobile-expo/src/types/index.ts | 1 + mobile-expo/src/types/navigation.types.ts | 2 +- mobile-expo/src/types/theme.types.ts | 10 ++++++ mobile-expo/tsconfig.json | 5 ++- 19 files changed, 70 insertions(+), 23 deletions(-) create mode 100644 mobile-expo/babel.config.js create mode 100644 mobile-expo/src/providers/ThemeProvider.tsx create mode 100644 mobile-expo/src/types/theme.types.ts diff --git a/mobile-expo/babel.config.js b/mobile-expo/babel.config.js new file mode 100644 index 0000000..9ccbb40 --- /dev/null +++ b/mobile-expo/babel.config.js @@ -0,0 +1,33 @@ +module.exports = function (api) { + api.cache(true); + return { + presets: ['babel-preset-expo'], + plugins: [ + [ + 'module-resolver', + { + root: ['./src'], + alias: { + '@': './src', + }, + extensions: [ + '.ios.ts', + '.android.ts', + '.native.ts', + '.web.ts', + '.ts', + '.ios.tsx', + '.android.tsx', + '.native.tsx', + '.web.tsx', + '.tsx', + '.jsx', + '.js', + '.json', + ], + }, + ], + ], + }; +}; + diff --git a/mobile-expo/package.json b/mobile-expo/package.json index f886be9..5f987b7 100644 --- a/mobile-expo/package.json +++ b/mobile-expo/package.json @@ -36,6 +36,7 @@ "devDependencies": { "@types/react": "~19.1.0", "@types/react-native": "^0.72.8", + "babel-plugin-module-resolver": "^5.0.2", "eslint": "^9.39.1", "eslint-config-prettier": "^10.1.8", "prettier": "^3.7.4", diff --git a/mobile-expo/src/hooks/tasks/index.ts b/mobile-expo/src/hooks/tasks/index.ts index 1688f8e..e06c859 100644 --- a/mobile-expo/src/hooks/tasks/index.ts +++ b/mobile-expo/src/hooks/tasks/index.ts @@ -5,9 +5,9 @@ import { TIdTask, TTask, UpdateTaskPayload, -} from '../../types'; -import { tasksApi } from '../../services'; -import { CONSTANTS } from '../../constants'; +} from '@/types'; +import { tasksApi } from '@/services'; +import { CONSTANTS } from '@/constants'; export const tasksKeysConfig = { all: ['tasks'], diff --git a/mobile-expo/src/navigation/SettingsStackNavigator.tsx b/mobile-expo/src/navigation/SettingsStackNavigator.tsx index 9852ad2..48c6ad1 100644 --- a/mobile-expo/src/navigation/SettingsStackNavigator.tsx +++ b/mobile-expo/src/navigation/SettingsStackNavigator.tsx @@ -1,6 +1,6 @@ import { createNativeStackNavigator } from '@react-navigation/native-stack'; -import { SettingsListScreen } from '../screens'; -import { CustomHeader } from '../components'; +import { SettingsListScreen } from '@/screens'; +import { CustomHeader } from '@/components'; export type SettingsStackParamList = { SettingList: undefined; diff --git a/mobile-expo/src/navigation/TasksStackNavigator.tsx b/mobile-expo/src/navigation/TasksStackNavigator.tsx index 195d0b7..bab8cf2 100644 --- a/mobile-expo/src/navigation/TasksStackNavigator.tsx +++ b/mobile-expo/src/navigation/TasksStackNavigator.tsx @@ -1,6 +1,6 @@ import { createNativeStackNavigator } from '@react-navigation/native-stack'; -import { CreateTaskScreen, EditTaskScreen, TasksListScreen } from '../screens'; -import { CustomHeader } from '../components'; +import { CreateTaskScreen, EditTaskScreen, TasksListScreen } from '@/screens'; +import { CustomHeader } from '@/components'; export type TasksStackParamList = { TasksList: undefined; diff --git a/mobile-expo/src/providers/TanstackProvider.tsx b/mobile-expo/src/providers/TanstackProvider.tsx index dde7051..646f809 100644 --- a/mobile-expo/src/providers/TanstackProvider.tsx +++ b/mobile-expo/src/providers/TanstackProvider.tsx @@ -1,6 +1,6 @@ import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; import { ReactNode } from 'react'; -import { CONSTANTS } from '../constants'; +import { CONSTANTS } from '@/constants'; const queryClient = new QueryClient({ defaultOptions: { diff --git a/mobile-expo/src/providers/ThemeProvider.tsx b/mobile-expo/src/providers/ThemeProvider.tsx new file mode 100644 index 0000000..139597f --- /dev/null +++ b/mobile-expo/src/providers/ThemeProvider.tsx @@ -0,0 +1,2 @@ + + diff --git a/mobile-expo/src/providers/index.ts b/mobile-expo/src/providers/index.ts index 41444b3..586a3df 100644 --- a/mobile-expo/src/providers/index.ts +++ b/mobile-expo/src/providers/index.ts @@ -1,2 +1,3 @@ export * from './TanstackProvider'; export * from './AppProvider'; +export * from './ThemeProvider'; diff --git a/mobile-expo/src/screens/settings/SettingsListScreen.tsx b/mobile-expo/src/screens/settings/SettingsListScreen.tsx index a455a34..602599c 100644 --- a/mobile-expo/src/screens/settings/SettingsListScreen.tsx +++ b/mobile-expo/src/screens/settings/SettingsListScreen.tsx @@ -1,5 +1,5 @@ import { Text } from "react-native"; -import { ScreenWrapper } from "../../components"; +import { ScreenWrapper } from "@/components"; export const SettingsListScreen = () => { return ( diff --git a/mobile-expo/src/screens/tasks/CreateTaskScreen.tsx b/mobile-expo/src/screens/tasks/CreateTaskScreen.tsx index a8b3142..f61844f 100644 --- a/mobile-expo/src/screens/tasks/CreateTaskScreen.tsx +++ b/mobile-expo/src/screens/tasks/CreateTaskScreen.tsx @@ -1,5 +1,5 @@ import { Text } from "react-native"; -import { ScreenWrapper } from "../../components"; +import { ScreenWrapper } from "@/components"; export const CreateTaskScreen = () => { return ( diff --git a/mobile-expo/src/screens/tasks/EditTaskScreen.tsx b/mobile-expo/src/screens/tasks/EditTaskScreen.tsx index 3aed6dc..c8ccece 100644 --- a/mobile-expo/src/screens/tasks/EditTaskScreen.tsx +++ b/mobile-expo/src/screens/tasks/EditTaskScreen.tsx @@ -1,5 +1,5 @@ import { Text } from 'react-native'; -import { ScreenWrapper } from '../../components'; +import { ScreenWrapper } from '@/components'; export const EditTaskScreen = () => { return ( diff --git a/mobile-expo/src/screens/tasks/TasksListScreen.tsx b/mobile-expo/src/screens/tasks/TasksListScreen.tsx index 3aaabb7..e92b617 100644 --- a/mobile-expo/src/screens/tasks/TasksListScreen.tsx +++ b/mobile-expo/src/screens/tasks/TasksListScreen.tsx @@ -1,6 +1,6 @@ import { FlatList, Text, View } from "react-native"; -import { ScreenWrapper } from "../../components"; -import { useTasks } from "../../hooks"; +import { ScreenWrapper } from "@/components"; +import { useTasks } from "@/hooks"; export const TasksListScreen = () => { const { data: tasks, isLoading } = useTasks(); diff --git a/mobile-expo/src/services/api/apiClient.ts b/mobile-expo/src/services/api/apiClient.ts index 75ed796..ad683f7 100644 --- a/mobile-expo/src/services/api/apiClient.ts +++ b/mobile-expo/src/services/api/apiClient.ts @@ -1,6 +1,6 @@ import axios from "axios"; import { Platform } from "react-native"; -import { CONSTANTS } from "../../constants"; +import { CONSTANTS } from "@/constants"; const getBaseUrl = () => { const envUrl = process.env.NEST_BASE_URL || CONSTANTS.DEFAULT_URL; diff --git a/mobile-expo/src/services/api/tasksApi.ts b/mobile-expo/src/services/api/tasksApi.ts index 5103582..f2b674b 100644 --- a/mobile-expo/src/services/api/tasksApi.ts +++ b/mobile-expo/src/services/api/tasksApi.ts @@ -4,7 +4,7 @@ import { TIdTask, TTask, UpdateTaskPayload, -} from "../../types"; +} from "@/types"; import { apiClient } from "./apiClient"; const TASKS_ROUTE = "/tasks"; diff --git a/mobile-expo/src/styles/theme/index.ts b/mobile-expo/src/styles/theme/index.ts index d880233..e50ef11 100644 --- a/mobile-expo/src/styles/theme/index.ts +++ b/mobile-expo/src/styles/theme/index.ts @@ -1,7 +1,3 @@ -import { TDarkTheme } from './dark'; -import { TLightTheme } from './light'; +export * from './dark'; +export * from './light'; -export { lightTheme } from './light'; -export { darkTheme } from './dark'; - -export type TTheme = TLightTheme | TDarkTheme; diff --git a/mobile-expo/src/types/index.ts b/mobile-expo/src/types/index.ts index 6a608dc..2c939b8 100644 --- a/mobile-expo/src/types/index.ts +++ b/mobile-expo/src/types/index.ts @@ -1,2 +1,3 @@ export * from './navigation.types'; export * from './task.types'; +export * from './theme.types'; diff --git a/mobile-expo/src/types/navigation.types.ts b/mobile-expo/src/types/navigation.types.ts index 3899688..cd6f8e5 100644 --- a/mobile-expo/src/types/navigation.types.ts +++ b/mobile-expo/src/types/navigation.types.ts @@ -1,4 +1,4 @@ -import { BottomTabsParamList } from '../navigation/BottomTabsNavigator'; +import { BottomTabsParamList } from '@/navigation/BottomTabsNavigator'; declare global { namespace ReactNavigation { diff --git a/mobile-expo/src/types/theme.types.ts b/mobile-expo/src/types/theme.types.ts new file mode 100644 index 0000000..6fe3580 --- /dev/null +++ b/mobile-expo/src/types/theme.types.ts @@ -0,0 +1,10 @@ +import { TDarkTheme } from "@/styles/theme/dark"; +import { TLightTheme } from "@/styles/theme/light"; + +export const enum EnumThemeMode { + light = 'light', + dark = 'dark', + auto = 'auto', +} + +export type TTheme = TLightTheme | TDarkTheme; diff --git a/mobile-expo/tsconfig.json b/mobile-expo/tsconfig.json index b9567f6..a354d97 100644 --- a/mobile-expo/tsconfig.json +++ b/mobile-expo/tsconfig.json @@ -1,6 +1,9 @@ { "extends": "expo/tsconfig.base", "compilerOptions": { - "strict": true + "strict": true, + "paths": { + "@/*": ["./src/*"] + } } } From 64c28021c9b095eafa6a839b7006ec69b04e119a Mon Sep 17 00:00:00 2001 From: Meleshko Dmitriy Date: Sat, 20 Dec 2025 18:28:24 +0300 Subject: [PATCH 13/18] feat: theme provider, context, hook --- mobile-expo/src/hooks/index.ts | 2 +- mobile-expo/src/hooks/theme/index.ts | 12 ++++ mobile-expo/src/providers/ThemeProvider.tsx | 61 +++++++++++++++++++++ mobile-expo/src/types/theme.types.ts | 10 +--- 4 files changed, 77 insertions(+), 8 deletions(-) create mode 100644 mobile-expo/src/hooks/theme/index.ts diff --git a/mobile-expo/src/hooks/index.ts b/mobile-expo/src/hooks/index.ts index 3c78011..edfd870 100644 --- a/mobile-expo/src/hooks/index.ts +++ b/mobile-expo/src/hooks/index.ts @@ -1,2 +1,2 @@ export * from './tasks'; - +export * from './theme'; diff --git a/mobile-expo/src/hooks/theme/index.ts b/mobile-expo/src/hooks/theme/index.ts new file mode 100644 index 0000000..2f39729 --- /dev/null +++ b/mobile-expo/src/hooks/theme/index.ts @@ -0,0 +1,12 @@ +import { ThemeContext } from '@/providers'; +import { useContext } from 'react'; + +export const useTheme = () => { + const themeContext = useContext(ThemeContext); + + if (!themeContext) { + throw new Error('useTheme must be used within ThemeProvider'); + } + + return themeContext; +}; diff --git a/mobile-expo/src/providers/ThemeProvider.tsx b/mobile-expo/src/providers/ThemeProvider.tsx index 139597f..4d088c1 100644 --- a/mobile-expo/src/providers/ThemeProvider.tsx +++ b/mobile-expo/src/providers/ThemeProvider.tsx @@ -1,2 +1,63 @@ +import { darkTheme, lightTheme } from '@/styles'; +import { TThemeMode, TTheme } from '@/types'; +import { createContext, ReactNode, useState } from 'react'; +import { useColorScheme } from 'react-native'; +type TThemeContext = { + theme: TTheme; + themeMode: TThemeMode; + setThemeMode: (mode: TThemeMode) => void; + toggleTheme: () => void; + isDarkTheme: boolean; +}; +export const ThemeContext = createContext(undefined); + +interface ThemeProviderProps { + children: ReactNode; + initialMode?: TThemeMode; +} + +export const ThemeProvider = ({ + children, + initialMode = 'auto', +}: ThemeProviderProps) => { + const systemColorSchema = useColorScheme(); + const [themeMode, setThemeMode] = useState(initialMode); + + const getCurrentTheme = (): TTheme => { + if (themeMode === 'auto') { + return systemColorSchema === 'dark' ? darkTheme : lightTheme; + } + + return themeMode === 'dark' ? darkTheme : lightTheme; + }; + + const theme = getCurrentTheme(); + + const isDarkTheme = theme === darkTheme; + + const toggleTheme = () => { + setThemeMode((prev) => { + if (prev === 'auto') { + return systemColorSchema === 'dark' ? 'light' : 'dark'; + } + + return prev === 'dark' ? 'light' : 'dark'; + }); + }; + + return ( + + {children} + + ); +}; diff --git a/mobile-expo/src/types/theme.types.ts b/mobile-expo/src/types/theme.types.ts index 6fe3580..0d9368e 100644 --- a/mobile-expo/src/types/theme.types.ts +++ b/mobile-expo/src/types/theme.types.ts @@ -1,10 +1,6 @@ -import { TDarkTheme } from "@/styles/theme/dark"; -import { TLightTheme } from "@/styles/theme/light"; +import { TDarkTheme } from '@/styles/theme/dark'; +import { TLightTheme } from '@/styles/theme/light'; -export const enum EnumThemeMode { - light = 'light', - dark = 'dark', - auto = 'auto', -} +export type TThemeMode = 'auto' | 'light' | 'dark'; export type TTheme = TLightTheme | TDarkTheme; From 3d40935a410dc555b144614e1fbf03f1581d57d7 Mon Sep 17 00:00:00 2001 From: Meleshko Dmitriy Date: Sat, 20 Dec 2025 20:22:22 +0300 Subject: [PATCH 14/18] feat: ui button & text --- .../common/UIComponents/UIButton/index.tsx | 69 +++++++++++++++++++ .../common/UIComponents/UIButton/types.ts | 5 ++ .../common/UIComponents/UIButton/utils.ts | 35 ++++++++++ .../common/UIComponents/UIText/index.tsx | 43 ++++++++++++ .../components/common/UIComponents/index.ts | 2 + mobile-expo/src/components/common/index.ts | 3 +- mobile-expo/src/providers/AppProvider.tsx | 7 +- mobile-expo/src/providers/ThemeProvider.tsx | 6 +- mobile-expo/src/styles/theme/dark.ts | 47 +++++++------ mobile-expo/src/styles/theme/light.ts | 47 +++++++------ mobile-expo/src/styles/typography.ts | 12 ++-- mobile-expo/src/types/theme.types.ts | 38 ++++++++-- 12 files changed, 256 insertions(+), 58 deletions(-) create mode 100644 mobile-expo/src/components/common/UIComponents/UIButton/index.tsx create mode 100644 mobile-expo/src/components/common/UIComponents/UIButton/types.ts create mode 100644 mobile-expo/src/components/common/UIComponents/UIButton/utils.ts create mode 100644 mobile-expo/src/components/common/UIComponents/UIText/index.tsx create mode 100644 mobile-expo/src/components/common/UIComponents/index.ts diff --git a/mobile-expo/src/components/common/UIComponents/UIButton/index.tsx b/mobile-expo/src/components/common/UIComponents/UIButton/index.tsx new file mode 100644 index 0000000..549330e --- /dev/null +++ b/mobile-expo/src/components/common/UIComponents/UIButton/index.tsx @@ -0,0 +1,69 @@ +import { + ActivityIndicator, + StyleSheet, + TouchableOpacity, + TouchableOpacityProps, +} from 'react-native'; +import { EnumUIButtonVariants } from './types'; +import { getUIButtonStyles, getUIButtonTextColor } from './utils'; +import { useTheme } from '@/hooks'; +import { UIText } from '../UIText'; +import { TTextColor } from '@/types'; +import { spacing } from '@/styles'; + +interface UIButtonProps extends Omit { + title: string; + onPress: () => void; + variant?: EnumUIButtonVariants; + isLoading?: boolean; + isDisabled?: boolean; +} + +export const UIButton = ({ + title, + onPress, + variant = EnumUIButtonVariants.PRIMARY, + isLoading = false, + isDisabled = false, + ...props +}: UIButtonProps) => { + const { theme } = useTheme(); + + const indicatorColor = getUIButtonTextColor(variant, theme); + const textColor: TTextColor = + variant === EnumUIButtonVariants.PRIMARY ? 'primary' : 'secondary'; + const buttonStyles = getUIButtonStyles(variant, theme); + + const buttonContent = () => { + return isLoading ? ( + + ) : ( + + ); + }; + + return ( + + {buttonContent()} + + ); +}; + +const styles = StyleSheet.create({ + button: { + paddingVertical: spacing.xs, + paddingHorizontal: spacing.sm, + alignItems: 'center', + justifyContent: 'center', + borderRadius: 8, + }, + disabled: { + opacity: 0.5, + }, +}); diff --git a/mobile-expo/src/components/common/UIComponents/UIButton/types.ts b/mobile-expo/src/components/common/UIComponents/UIButton/types.ts new file mode 100644 index 0000000..8e59540 --- /dev/null +++ b/mobile-expo/src/components/common/UIComponents/UIButton/types.ts @@ -0,0 +1,5 @@ +export const enum EnumUIButtonVariants { + PRIMARY = 'primary', + SECONDARY = 'secondary', + OUTLINE = 'outline', +} diff --git a/mobile-expo/src/components/common/UIComponents/UIButton/utils.ts b/mobile-expo/src/components/common/UIComponents/UIButton/utils.ts new file mode 100644 index 0000000..8de267c --- /dev/null +++ b/mobile-expo/src/components/common/UIComponents/UIButton/utils.ts @@ -0,0 +1,35 @@ +import { Theme } from '@/types'; +import { EnumUIButtonVariants } from './types'; +import { ViewStyle } from 'react-native'; + +export const getUIButtonTextColor = ( + variant: EnumUIButtonVariants, + theme: Theme +): string => { + switch (variant) { + case EnumUIButtonVariants.PRIMARY: + return theme.colors.text.primary; + case EnumUIButtonVariants.SECONDARY: + return theme.colors.text.secondary; + case EnumUIButtonVariants.OUTLINE: + return theme.colors.text.secondary; + default: + return theme.colors.text.primary; + } +}; + +export const getUIButtonStyles = ( + variant: EnumUIButtonVariants, + theme: Theme +): ViewStyle => { + switch (variant) { + case EnumUIButtonVariants.PRIMARY: + return { backgroundColor: theme.colors.base.primaryDark }; + case EnumUIButtonVariants.SECONDARY: + return { backgroundColor: theme.colors.base.primary }; + case EnumUIButtonVariants.OUTLINE: + return { backgroundColor: theme.colors.base.primaryLight }; + default: + return { backgroundColor: theme.colors.base.primaryDark }; + } +}; diff --git a/mobile-expo/src/components/common/UIComponents/UIText/index.tsx b/mobile-expo/src/components/common/UIComponents/UIText/index.tsx new file mode 100644 index 0000000..9523605 --- /dev/null +++ b/mobile-expo/src/components/common/UIComponents/UIText/index.tsx @@ -0,0 +1,43 @@ +import { useTheme } from '@/hooks'; +import { TFontSize, TFontWeight, typography } from '@/styles'; +import { TTextColor } from '@/types'; +import { StyleSheet, Text, TextProps } from 'react-native'; + +interface UITextProps extends Omit { + text: string + size?: TFontSize; + weight?: TFontWeight; + color?: TTextColor; +} + +export const UIText = ({ + text, + size = 'md', + weight = 'regular', + color = 'primary', + ...props +}: UITextProps) => { + const { theme } = useTheme(); + + const textStyles = [ + styles.text, + { + fontSize: typography.fontSize[size], + fontWeight: typography.fontWeight[weight], + color: theme.colors.text[color], + }, + ]; + + return ( + + {text} + + ); +}; + +const styles = StyleSheet.create({ + text: { + fontSize: typography.fontSize.md, + fontWeight: typography.fontWeight.regular, + }, +}); diff --git a/mobile-expo/src/components/common/UIComponents/index.ts b/mobile-expo/src/components/common/UIComponents/index.ts new file mode 100644 index 0000000..2eda2e4 --- /dev/null +++ b/mobile-expo/src/components/common/UIComponents/index.ts @@ -0,0 +1,2 @@ +export * from './UIText'; +export * from './UIButton'; diff --git a/mobile-expo/src/components/common/index.ts b/mobile-expo/src/components/common/index.ts index 3067b44..14d98ca 100644 --- a/mobile-expo/src/components/common/index.ts +++ b/mobile-expo/src/components/common/index.ts @@ -1,3 +1,4 @@ export * from './ScreenWrapper'; export * from './ErrorBoundary'; -export * from './CustomHeader' \ No newline at end of file +export * from './CustomHeader'; +export * from './UIComponents'; diff --git a/mobile-expo/src/providers/AppProvider.tsx b/mobile-expo/src/providers/AppProvider.tsx index 6476912..dbf1ace 100644 --- a/mobile-expo/src/providers/AppProvider.tsx +++ b/mobile-expo/src/providers/AppProvider.tsx @@ -2,6 +2,7 @@ import { ReactNode } from 'react'; import { SafeAreaProvider } from 'react-native-safe-area-context'; import { StatusBar } from 'expo-status-bar'; import { TanstackProvider } from './TanstackProvider'; +import { ThemeProvider } from './ThemeProvider'; interface AppProviderProps { children: ReactNode; @@ -10,8 +11,10 @@ interface AppProviderProps { export const AppProvider = ({ children }: AppProviderProps) => { return ( - - {children} + + + {children} + ); }; diff --git a/mobile-expo/src/providers/ThemeProvider.tsx b/mobile-expo/src/providers/ThemeProvider.tsx index 4d088c1..142e56f 100644 --- a/mobile-expo/src/providers/ThemeProvider.tsx +++ b/mobile-expo/src/providers/ThemeProvider.tsx @@ -1,10 +1,10 @@ import { darkTheme, lightTheme } from '@/styles'; -import { TThemeMode, TTheme } from '@/types'; +import { Theme, TThemeMode } from '@/types'; import { createContext, ReactNode, useState } from 'react'; import { useColorScheme } from 'react-native'; type TThemeContext = { - theme: TTheme; + theme: Theme; themeMode: TThemeMode; setThemeMode: (mode: TThemeMode) => void; toggleTheme: () => void; @@ -25,7 +25,7 @@ export const ThemeProvider = ({ const systemColorSchema = useColorScheme(); const [themeMode, setThemeMode] = useState(initialMode); - const getCurrentTheme = (): TTheme => { + const getCurrentTheme = (): Theme => { if (themeMode === 'auto') { return systemColorSchema === 'dark' ? darkTheme : lightTheme; } diff --git a/mobile-expo/src/styles/theme/dark.ts b/mobile-expo/src/styles/theme/dark.ts index 3376b08..01e597d 100644 --- a/mobile-expo/src/styles/theme/dark.ts +++ b/mobile-expo/src/styles/theme/dark.ts @@ -1,23 +1,30 @@ -export const darkTheme = { +import { Theme } from "@/types"; + +export const darkTheme: Theme = { colors: { - primary: '#BB86FC', - primaryDark: '#3700B3', - primaryLight: '#6200EE', - background: '#121212', - text: '#FFFFFF', - textSecondary: 'rgba(255, 255, 255, 0.7)', - - priorityLow: '#66BB6A', - priorityMedium: '#FFA726', - priorityHigh: '#EF5350', - - statusTodo: '#757575', - statusInProgress: '#42A5F5', - statusDone: '#66BB6A', - - overlay: 'rgba(0, 0, 0, 0.7)', - backdrop: 'rgba(0, 0, 0, 0.5)', + base: { + primary: '#BB86FC', + primaryDark: '#3700B3', + primaryLight: '#6200EE', + }, + text: { + primary: '#FFFFFF', + secondary: 'rgba(255, 255, 255, 0.7)', + }, + priority: { + low: '#66BB6A', + medium: '#FFA726', + high: '#EF5350', + }, + status: { + todo: '#757575', + inProgress: '#42A5F5', + done: '#66BB6A', + }, + background: { + background: '#121212', + overlay: 'rgba(0, 0, 0, 0.7)', + backdrop: 'rgba(0, 0, 0, 0.5)', + }, }, } as const; - -export type TDarkTheme = typeof darkTheme; \ No newline at end of file diff --git a/mobile-expo/src/styles/theme/light.ts b/mobile-expo/src/styles/theme/light.ts index 7ebf670..4c202b7 100644 --- a/mobile-expo/src/styles/theme/light.ts +++ b/mobile-expo/src/styles/theme/light.ts @@ -1,23 +1,30 @@ -export const lightTheme = { +import { Theme } from "@/types"; + +export const lightTheme: Theme = { colors: { - primary: '#6200EE', - primaryDark: '#3700B3', - primaryLight: '#BB86FC', - background: '#FFFFFF', - text: '#000000', - textSecondary: 'rgba(0, 0, 0, 0.6)', - - priorityLow: '#4CAF50', - priorityMedium: '#FF9800', - priorityHigh: '#F44336', - - statusTodo: '#9E9E9E', - statusInProgress: '#2196F3', - statusDone: '#4CAF50', - - overlay: 'rgba(0, 0, 0, 0.5)', - backdrop: 'rgba(0, 0, 0, 0.32)', + base: { + primary: '#6200EE', + primaryDark: '#3700B3', + primaryLight: '#BB86FC', + }, + text: { + primary: '#000000', + secondary: 'rgba(0, 0, 0, 0.6)', + }, + priority: { + low: '#4CAF50', + medium: '#FF9800', + high: '#F44336', + }, + status: { + todo: '#9E9E9E', + inProgress: '#2196F3', + done: '#4CAF50', + }, + background: { + background: '#FFFFFF', + overlay: 'rgba(0, 0, 0, 0.5)', + backdrop: 'rgba(0, 0, 0, 0.32)', + }, }, } as const; - -export type TLightTheme = typeof lightTheme; \ No newline at end of file diff --git a/mobile-expo/src/styles/typography.ts b/mobile-expo/src/styles/typography.ts index 9e381ee..957c338 100644 --- a/mobile-expo/src/styles/typography.ts +++ b/mobile-expo/src/styles/typography.ts @@ -9,16 +9,12 @@ export const typography = { xxxl: 32, }, fontWeight: { + thin: '200' as const, regular: '400' as const, - medium: '500' as const, - semibold: '600' as const, bold: '700' as const, }, - lineHeight: { - tight: 1.2, - normal: 1.5, - relaxed: 1.8, - }, } as const; -export type TTypography = typeof typography; \ No newline at end of file +export type TTypography = typeof typography; +export type TFontSize = keyof typeof typography.fontSize; +export type TFontWeight = keyof typeof typography.fontWeight; diff --git a/mobile-expo/src/types/theme.types.ts b/mobile-expo/src/types/theme.types.ts index 0d9368e..a92694a 100644 --- a/mobile-expo/src/types/theme.types.ts +++ b/mobile-expo/src/types/theme.types.ts @@ -1,6 +1,36 @@ -import { TDarkTheme } from '@/styles/theme/dark'; -import { TLightTheme } from '@/styles/theme/light'; - export type TThemeMode = 'auto' | 'light' | 'dark'; -export type TTheme = TLightTheme | TDarkTheme; +export type TThemeColors = { + base: { + primary: string; + primaryDark: string; + primaryLight: string; + }; + text: { + primary: string; + secondary: string; + }; + priority: { + low: string; + medium: string; + high: string; + }; + status: { + todo: string; + inProgress: string; + done: string; + }; + background: { + background: string; + overlay: string; + backdrop: string; + }; +}; + +export type Theme = { + colors: TThemeColors; +} + +export type TTextColor = keyof TThemeColors['text']; +export type TStatusColor = keyof TThemeColors['status']; +export type TPriorityColor = keyof TThemeColors['priority']; From 09911fc0126da2aaf4f8dbbfcc2408d2aef3e00f Mon Sep 17 00:00:00 2001 From: Meleshko Dmitriy Date: Sun, 21 Dec 2025 10:17:08 +0300 Subject: [PATCH 15/18] feat: UIText --- backend-nest/VALIDATORS_REFERENCE.md | 219 ------------------ .../common/UIComponents/UIInput/index.tsx | 22 ++ 2 files changed, 22 insertions(+), 219 deletions(-) delete mode 100644 backend-nest/VALIDATORS_REFERENCE.md create mode 100644 mobile-expo/src/components/common/UIComponents/UIInput/index.tsx diff --git a/backend-nest/VALIDATORS_REFERENCE.md b/backend-nest/VALIDATORS_REFERENCE.md deleted file mode 100644 index ffc5904..0000000 --- a/backend-nest/VALIDATORS_REFERENCE.md +++ /dev/null @@ -1,219 +0,0 @@ -# Список декораторов валидации для CreateTaskDto - -## Для строковых полей (title, description, category) - -### 1. **MinLength** - минимальная длина строки -```typescript -@MinLength(3, { message: 'Title must be at least 3 characters long' }) -title: string; -``` - -### 2. **MaxLength** - максимальная длина строки -```typescript -@MaxLength(100, { message: 'Title must not exceed 100 characters' }) -title: string; -``` - -### 3. **Length** - точная длина или диапазон -```typescript -@Length(3, 100, { message: 'Title must be between 3 and 100 characters' }) -title: string; -``` - -### 4. **Matches** - проверка по регулярному выражению -```typescript -@Matches(/^[a-zA-Z0-9\s]+$/, { message: 'Title can only contain letters, numbers and spaces' }) -title: string; -``` - -### 5. **IsAlphanumeric** - только буквы и цифры -```typescript -@IsAlphanumeric() -category: string; -``` - -### 6. **IsAlpha** - только буквы -```typescript -@IsAlpha() -category: string; -``` - -### 7. **IsUppercase** / **IsLowercase** - регистр -```typescript -@IsUppercase() -category: string; -``` - -### 8. **Contains** - содержит подстроку -```typescript -@Contains('task', { message: 'Title must contain "task"' }) -title: string; -``` - -### 9. **NotContains** - не содержит подстроку -```typescript -@NotContains('spam', { message: 'Title cannot contain "spam"' }) -title: string; -``` - -### 10. **IsUrl** - проверка URL -```typescript -@IsUrl() -attachmentUrl: string; -``` - -### 11. **IsEmail** - проверка email -```typescript -@IsEmail() -assigneeEmail: string; -``` - -### 12. **IsUUID** - проверка UUID -```typescript -@IsUUID() -userId: string; -``` - -### 13. **IsOptional** - опциональное поле -```typescript -@IsOptional() -@IsString() -description?: string; -``` - -### 14. **IsDefined** - поле должно быть определено -```typescript -@IsDefined() -title: string; -``` - -## Для enum полей (priority) - -### 15. **IsIn** - значение из списка (альтернатива IsEnum) -```typescript -@IsIn(['low', 'medium', 'high']) -priority: string; -``` - -## Для числовых полей (если добавите) - -### 16. **IsNumber** - проверка числа -```typescript -@IsNumber() -estimatedHours: number; -``` - -### 17. **IsInt** - целое число -```typescript -@IsInt() -order: number; -``` - -### 18. **Min** - минимальное значение -```typescript -@Min(0, { message: 'Estimated hours must be at least 0' }) -estimatedHours: number; -``` - -### 19. **Max** - максимальное значение -```typescript -@Max(100, { message: 'Estimated hours must not exceed 100' }) -estimatedHours: number; -``` - -### 20. **IsPositive** - положительное число -```typescript -@IsPositive() -estimatedHours: number; -``` - -### 21. **IsNegative** - отрицательное число -```typescript -@IsNegative() -penalty: number; -``` - -## Для дат (если добавите) - -### 22. **IsDate** - проверка даты -```typescript -@IsDate() -dueDate: Date; -``` - -### 23. **IsDateString** - строка в формате даты -```typescript -@IsDateString() -dueDate: string; -``` - -### 24. **MinDate** - минимальная дата -```typescript -@MinDate(new Date(), { message: 'Due date must be in the future' }) -dueDate: Date; -``` - -### 25. **MaxDate** - максимальная дата -```typescript -@MaxDate(new Date('2025-12-31'), { message: 'Due date must be before 2026' }) -dueDate: Date; -``` - -## Для массивов (если добавите) - -### 26. **IsArray** - проверка массива -```typescript -@IsArray() -tags: string[]; -``` - -### 27. **ArrayMinSize** - минимальный размер массива -```typescript -@ArrayMinSize(1, { message: 'At least one tag is required' }) -tags: string[]; -``` - -### 28. **ArrayMaxSize** - максимальный размер массива -```typescript -@ArrayMaxSize(10, { message: 'Maximum 10 tags allowed' }) -tags: string[]; -``` - -### 29. **ArrayNotEmpty** - массив не пустой -```typescript -@ArrayNotEmpty() -tags: string[]; -``` - -## Для булевых значений (если добавите) - -### 30. **IsBoolean** - проверка булева значения -```typescript -@IsBoolean() -isCompleted: boolean; -``` - -## Комбинированные валидаторы - -### 31. **ValidateIf** - условная валидация -```typescript -@ValidateIf(o => o.priority === 'high') -@IsNotEmpty() -urgentReason: string; -``` - -### 32. **ValidateNested** - валидация вложенных объектов -```typescript -@ValidateNested() -@Type(() => AssigneeDto) -assignee: AssigneeDto; -``` - -### 33. **IsObject** - проверка объекта -```typescript -@IsObject() -metadata: Record; -``` - - - diff --git a/mobile-expo/src/components/common/UIComponents/UIInput/index.tsx b/mobile-expo/src/components/common/UIComponents/UIInput/index.tsx new file mode 100644 index 0000000..b31dc43 --- /dev/null +++ b/mobile-expo/src/components/common/UIComponents/UIInput/index.tsx @@ -0,0 +1,22 @@ +import { StyleSheet, TextInput, TextInputProps, View } from 'react-native'; +import { UIText } from '../UIText'; +import { spacing } from '@/styles'; + +interface UIInputProps extends Omit { + label?: string; +} + +export const UIInput = ({ label, ...props }: UIInputProps) => { + return ( + + {label && } + + + ); +}; + +const styles = StyleSheet.create({ + container: { + gap: spacing.sm, + }, +}); From 926b9680475dd1b0e5d701e0a6a9822728d54fea Mon Sep 17 00:00:00 2001 From: Meleshko Dmitriy Date: Sun, 21 Dec 2025 11:56:54 +0300 Subject: [PATCH 16/18] feat: theme toggle --- mobile-expo/package.json | 3 +- .../components/common/CustomHeader/index.tsx | 7 ++-- .../src/components/common/ErrorBoundary.tsx | 32 +++---------------- .../src/components/common/ScreenWrapper.tsx | 3 ++ .../common/UIComponents/UIBox/index.tsx | 26 +++++++++++++++ .../common/UIComponents/UISwitcher/index.tsx | 19 +++++++++++ .../components/common/UIComponents/index.ts | 3 ++ mobile-expo/src/components/index.ts | 3 +- .../src/components/tasks/TaskCard/index.tsx | 17 ++++++++++ mobile-expo/src/components/tasks/index.ts | 1 + .../src/navigation/BottomTabsNavigator.tsx | 10 ++++++ mobile-expo/src/providers/AppProvider.tsx | 2 -- mobile-expo/src/providers/ThemeProvider.tsx | 3 ++ .../screens/settings/SettingsListScreen.tsx | 11 ++++--- .../src/screens/tasks/CreateTaskScreen.tsx | 5 ++- .../src/screens/tasks/EditTaskScreen.tsx | 5 ++- .../src/screens/tasks/TasksListScreen.tsx | 16 ++++------ 17 files changed, 113 insertions(+), 53 deletions(-) create mode 100644 mobile-expo/src/components/common/UIComponents/UIBox/index.tsx create mode 100644 mobile-expo/src/components/common/UIComponents/UISwitcher/index.tsx create mode 100644 mobile-expo/src/components/tasks/TaskCard/index.tsx create mode 100644 mobile-expo/src/components/tasks/index.ts diff --git a/mobile-expo/package.json b/mobile-expo/package.json index 5f987b7..56d23f2 100644 --- a/mobile-expo/package.json +++ b/mobile-expo/package.json @@ -20,7 +20,7 @@ "@tanstack/react-query": "^5.90.12", "axios": "^1.13.2", "date-fns": "^4.1.0", - "expo": "~54.0.27", + "expo": "~54.0.30", "expo-status-bar": "~3.0.9", "react": "19.1.0", "react-error-boundary": "^6.0.0", @@ -37,6 +37,7 @@ "@types/react": "~19.1.0", "@types/react-native": "^0.72.8", "babel-plugin-module-resolver": "^5.0.2", + "babel-preset-expo": "^54.0.9", "eslint": "^9.39.1", "eslint-config-prettier": "^10.1.8", "prettier": "^3.7.4", diff --git a/mobile-expo/src/components/common/CustomHeader/index.tsx b/mobile-expo/src/components/common/CustomHeader/index.tsx index 73044de..f8d9b83 100644 --- a/mobile-expo/src/components/common/CustomHeader/index.tsx +++ b/mobile-expo/src/components/common/CustomHeader/index.tsx @@ -4,6 +4,8 @@ import { CustomHeaderButton, TCustomHeaderButton, } from './ui/CustomHeaderButton'; +import { UIText } from '../UIComponents'; +import { useTheme } from '@/hooks'; interface CustomHeaderProps { title?: string; @@ -17,12 +19,13 @@ export const CustomHeader = ({ rightButton, }: CustomHeaderProps) => { const insets = useSafeAreaInsets(); + const { theme } = useTheme(); return ( - {title} + {title && } diff --git a/mobile-expo/src/components/common/ErrorBoundary.tsx b/mobile-expo/src/components/common/ErrorBoundary.tsx index fa65987..a957bdb 100644 --- a/mobile-expo/src/components/common/ErrorBoundary.tsx +++ b/mobile-expo/src/components/common/ErrorBoundary.tsx @@ -1,5 +1,6 @@ import { Component, ReactNode } from 'react'; -import { View, Text, StyleSheet, TouchableOpacity } from 'react-native'; +import { View, StyleSheet } from 'react-native'; +import { UIButton, UIText } from './UIComponents'; interface Props { children: ReactNode; @@ -37,11 +38,9 @@ export class ErrorBoundary extends Component { return ( - Something went wrong - {this.state.error?.message} - - Try again - + + + ); } @@ -57,25 +56,4 @@ const styles = StyleSheet.create({ alignItems: 'center', padding: 20, }, - title: { - fontSize: 20, - fontWeight: 'bold', - marginBottom: 10, - }, - message: { - fontSize: 14, - color: '#666', - marginBottom: 20, - textAlign: 'center', - }, - button: { - backgroundColor: '#007AFF', - paddingHorizontal: 20, - paddingVertical: 10, - borderRadius: 8, - }, - buttonText: { - color: '#fff', - fontWeight: '600', - }, }); diff --git a/mobile-expo/src/components/common/ScreenWrapper.tsx b/mobile-expo/src/components/common/ScreenWrapper.tsx index 64850f1..f43bd60 100644 --- a/mobile-expo/src/components/common/ScreenWrapper.tsx +++ b/mobile-expo/src/components/common/ScreenWrapper.tsx @@ -1,3 +1,4 @@ +import { useTheme } from '@/hooks'; import { ReactNode } from 'react'; import { View, @@ -28,12 +29,14 @@ export const ScreenWrapper = ({ contentStyle, }: ScreenWrapperProps) => { const insets = useSafeAreaInsets(); + const { theme } = useTheme(); const containerStyle = [ styles.container, { paddingTop: safeAreaTop ? insets.top : 0, paddingBottom: safeAreaBottom ? insets.bottom : 0, + backgroundColor: theme.colors.background.background, }, style, ]; diff --git a/mobile-expo/src/components/common/UIComponents/UIBox/index.tsx b/mobile-expo/src/components/common/UIComponents/UIBox/index.tsx new file mode 100644 index 0000000..f13cdb6 --- /dev/null +++ b/mobile-expo/src/components/common/UIComponents/UIBox/index.tsx @@ -0,0 +1,26 @@ +import { useTheme } from '@/hooks'; +import { spacing } from '@/styles'; +import { ReactNode } from 'react'; +import { StyleSheet, View, ViewStyle, StyleProp } from 'react-native'; + +interface UIBoxProps { + children: ReactNode; + style?: StyleProp; +} + +export const UIBox = ({ children, style}: UIBoxProps) => { + const { theme } = useTheme(); + + const boxStyles = { + backgroundColor: theme.colors.base.primaryLight, + }; + + return {children}; +}; + +const styles = StyleSheet.create({ + container: { + padding: spacing.md, + borderRadius: spacing.md, + }, +}); diff --git a/mobile-expo/src/components/common/UIComponents/UISwitcher/index.tsx b/mobile-expo/src/components/common/UIComponents/UISwitcher/index.tsx new file mode 100644 index 0000000..5ca1c1f --- /dev/null +++ b/mobile-expo/src/components/common/UIComponents/UISwitcher/index.tsx @@ -0,0 +1,19 @@ +import { useTheme } from '@/hooks'; +import { Switch, SwitchProps } from 'react-native'; + +interface UISwitcherProps extends SwitchProps {} + +export const UISwitcher = (props: UISwitcherProps) => { + const { theme } = useTheme(); + + return ( + + ); +}; diff --git a/mobile-expo/src/components/common/UIComponents/index.ts b/mobile-expo/src/components/common/UIComponents/index.ts index 2eda2e4..71ee48c 100644 --- a/mobile-expo/src/components/common/UIComponents/index.ts +++ b/mobile-expo/src/components/common/UIComponents/index.ts @@ -1,2 +1,5 @@ export * from './UIText'; export * from './UIButton'; +export * from './UIText'; +export * from './UIBox'; +export * from './UISwitcher'; diff --git a/mobile-expo/src/components/index.ts b/mobile-expo/src/components/index.ts index 89a3196..7cdf99b 100644 --- a/mobile-expo/src/components/index.ts +++ b/mobile-expo/src/components/index.ts @@ -1 +1,2 @@ -export * from './common' +export * from './common'; +export * from './tasks'; diff --git a/mobile-expo/src/components/tasks/TaskCard/index.tsx b/mobile-expo/src/components/tasks/TaskCard/index.tsx new file mode 100644 index 0000000..33c0318 --- /dev/null +++ b/mobile-expo/src/components/tasks/TaskCard/index.tsx @@ -0,0 +1,17 @@ +import { FC } from 'react'; +import { UIBox, UIText } from '../../common'; +import { TTask } from '@/types'; + +interface TaskCardProps { + task: TTask; +} + +export const TaskCard: FC = ({ task }) => { + const { title, priority, status, isBlocked } = task; + + return ( + + + + ); +}; diff --git a/mobile-expo/src/components/tasks/index.ts b/mobile-expo/src/components/tasks/index.ts new file mode 100644 index 0000000..8f6e91d --- /dev/null +++ b/mobile-expo/src/components/tasks/index.ts @@ -0,0 +1 @@ +export * from './TaskCard'; diff --git a/mobile-expo/src/navigation/BottomTabsNavigator.tsx b/mobile-expo/src/navigation/BottomTabsNavigator.tsx index a0d6bf7..d4d4239 100644 --- a/mobile-expo/src/navigation/BottomTabsNavigator.tsx +++ b/mobile-expo/src/navigation/BottomTabsNavigator.tsx @@ -1,6 +1,7 @@ import { createBottomTabNavigator } from '@react-navigation/bottom-tabs'; import { TasksStackNavigator } from './TasksStackNavigator'; import { SettingsStackNavigator } from './SettingsStackNavigator'; +import { useTheme } from '@/hooks'; export type BottomTabsParamList = { TasksStack: undefined; @@ -10,10 +11,19 @@ export type BottomTabsParamList = { const Tab = createBottomTabNavigator(); export const BottomTabsNavigator = () => { + const { theme } = useTheme(); + return ( { return ( - {children} diff --git a/mobile-expo/src/providers/ThemeProvider.tsx b/mobile-expo/src/providers/ThemeProvider.tsx index 142e56f..ec69f92 100644 --- a/mobile-expo/src/providers/ThemeProvider.tsx +++ b/mobile-expo/src/providers/ThemeProvider.tsx @@ -2,6 +2,7 @@ import { darkTheme, lightTheme } from '@/styles'; import { Theme, TThemeMode } from '@/types'; import { createContext, ReactNode, useState } from 'react'; import { useColorScheme } from 'react-native'; +import { StatusBar } from 'expo-status-bar'; type TThemeContext = { theme: Theme; @@ -36,6 +37,7 @@ export const ThemeProvider = ({ const theme = getCurrentTheme(); const isDarkTheme = theme === darkTheme; + const statusBarStyle = isDarkTheme ? 'light' : 'dark'; const toggleTheme = () => { setThemeMode((prev) => { @@ -57,6 +59,7 @@ export const ThemeProvider = ({ isDarkTheme, }} > + {children} ); diff --git a/mobile-expo/src/screens/settings/SettingsListScreen.tsx b/mobile-expo/src/screens/settings/SettingsListScreen.tsx index 602599c..0e8520c 100644 --- a/mobile-expo/src/screens/settings/SettingsListScreen.tsx +++ b/mobile-expo/src/screens/settings/SettingsListScreen.tsx @@ -1,10 +1,13 @@ -import { Text } from "react-native"; -import { ScreenWrapper } from "@/components"; +import { ScreenWrapper, UISwitcher, UIText } from '@/components'; +import { useTheme } from '@/hooks'; export const SettingsListScreen = () => { + const { isDarkTheme, toggleTheme } = useTheme(); + return ( - SettingsListScreen + + ); -}; \ No newline at end of file +}; diff --git a/mobile-expo/src/screens/tasks/CreateTaskScreen.tsx b/mobile-expo/src/screens/tasks/CreateTaskScreen.tsx index f61844f..e9bb79b 100644 --- a/mobile-expo/src/screens/tasks/CreateTaskScreen.tsx +++ b/mobile-expo/src/screens/tasks/CreateTaskScreen.tsx @@ -1,10 +1,9 @@ -import { Text } from "react-native"; -import { ScreenWrapper } from "@/components"; +import { ScreenWrapper, UIText } from "@/components"; export const CreateTaskScreen = () => { return ( - CreateTaskScreen + ); }; \ No newline at end of file diff --git a/mobile-expo/src/screens/tasks/EditTaskScreen.tsx b/mobile-expo/src/screens/tasks/EditTaskScreen.tsx index c8ccece..adb224e 100644 --- a/mobile-expo/src/screens/tasks/EditTaskScreen.tsx +++ b/mobile-expo/src/screens/tasks/EditTaskScreen.tsx @@ -1,10 +1,9 @@ -import { Text } from 'react-native'; -import { ScreenWrapper } from '@/components'; +import { ScreenWrapper, UIText } from '@/components'; export const EditTaskScreen = () => { return ( - EditTaskScreen + ); }; diff --git a/mobile-expo/src/screens/tasks/TasksListScreen.tsx b/mobile-expo/src/screens/tasks/TasksListScreen.tsx index e92b617..8634b10 100644 --- a/mobile-expo/src/screens/tasks/TasksListScreen.tsx +++ b/mobile-expo/src/screens/tasks/TasksListScreen.tsx @@ -1,6 +1,6 @@ -import { FlatList, Text, View } from "react-native"; -import { ScreenWrapper } from "@/components"; -import { useTasks } from "@/hooks"; +import { FlatList, Text, View } from 'react-native'; +import { ScreenWrapper, TaskCard, UIText } from '@/components'; +import { useTasks } from '@/hooks'; export const TasksListScreen = () => { const { data: tasks, isLoading } = useTasks(); @@ -8,22 +8,18 @@ export const TasksListScreen = () => { if (isLoading) { return ( - Loading.... + ); } return ( - TasksListScreenTasksListScreen + task.id} - renderItem={({ item }) => ( - - {item.title} - - )} + renderItem={({ item }) => } /> ); From 2a9ee628e488c5f5866c075a25abcc2d68c791e6 Mon Sep 17 00:00:00 2001 From: Meleshko Dmitriy Date: Sun, 21 Dec 2025 12:06:32 +0300 Subject: [PATCH 17/18] fix: errors --- mobile-expo/src/components/common/ErrorBoundary.tsx | 11 ++++++----- .../components/common/UIComponents/UIButton/index.tsx | 2 +- mobile-expo/src/screens/tasks/TasksListScreen.tsx | 3 ++- mobile-expo/src/types/task.types.ts | 4 ++-- 4 files changed, 11 insertions(+), 9 deletions(-) diff --git a/mobile-expo/src/components/common/ErrorBoundary.tsx b/mobile-expo/src/components/common/ErrorBoundary.tsx index a957bdb..33af3f4 100644 --- a/mobile-expo/src/components/common/ErrorBoundary.tsx +++ b/mobile-expo/src/components/common/ErrorBoundary.tsx @@ -1,6 +1,5 @@ import { Component, ReactNode } from 'react'; -import { View, StyleSheet } from 'react-native'; -import { UIButton, UIText } from './UIComponents'; +import { View, StyleSheet, Text, TouchableOpacity } from 'react-native'; interface Props { children: ReactNode; @@ -38,9 +37,11 @@ export class ErrorBoundary extends Component { return ( - - - + Something went wrong + {this.state.error?.message} + + Try again + ); } diff --git a/mobile-expo/src/components/common/UIComponents/UIButton/index.tsx b/mobile-expo/src/components/common/UIComponents/UIButton/index.tsx index 549330e..a578e21 100644 --- a/mobile-expo/src/components/common/UIComponents/UIButton/index.tsx +++ b/mobile-expo/src/components/common/UIComponents/UIButton/index.tsx @@ -44,7 +44,7 @@ export const UIButton = ({ return ( { - const { data: tasks, isLoading } = useTasks(); + const { data: tasks, isLoading, error } = useTasks(); if (isLoading) { return ( @@ -13,6 +13,7 @@ export const TasksListScreen = () => { ); } + return ( diff --git a/mobile-expo/src/types/task.types.ts b/mobile-expo/src/types/task.types.ts index c16812f..eb69835 100644 --- a/mobile-expo/src/types/task.types.ts +++ b/mobile-expo/src/types/task.types.ts @@ -36,8 +36,8 @@ export type TTask = { priority: EnumTaskPriority; status: EnumTaskStatus; isBlocked: boolean; - createdAt: Date; - updateAt: Date; + createdAt: string; + updateAt: string; }; export type TIdTask = TTask['id']; From 8218be0db4b25296ebd9334b8040921bc943150b Mon Sep 17 00:00:00 2001 From: Meleshko Dmitriy Date: Sun, 21 Dec 2025 16:12:05 +0300 Subject: [PATCH 18/18] feat: priority, status, blocked, card --- .../components/common/CustomHeader/index.tsx | 2 +- .../CustomHeader/ui/CustomHeaderButton.tsx | 1 + .../common/UIComponents/UIBox/index.tsx | 1 + .../components/tasks/TaskBlocked/index.tsx | 16 ++++++++ .../src/components/tasks/TaskCard/index.tsx | 18 +++++++++ .../components/tasks/TaskPriority/index.tsx | 37 +++++++++++++++++++ .../components/tasks/TaskPriority/utils.ts | 29 +++++++++++++++ .../src/components/tasks/TaskStatus/index.tsx | 34 +++++++++++++++++ .../src/components/tasks/TaskStatus/utils.ts | 26 +++++++++++++ mobile-expo/src/components/tasks/index.ts | 3 ++ mobile-expo/src/styles/theme/dark.ts | 4 +- mobile-expo/src/styles/theme/light.ts | 2 +- mobile-expo/src/utils/capitalizeString.ts | 3 ++ mobile-expo/src/utils/index.ts | 1 + 14 files changed, 173 insertions(+), 4 deletions(-) create mode 100644 mobile-expo/src/components/tasks/TaskBlocked/index.tsx create mode 100644 mobile-expo/src/components/tasks/TaskPriority/index.tsx create mode 100644 mobile-expo/src/components/tasks/TaskPriority/utils.ts create mode 100644 mobile-expo/src/components/tasks/TaskStatus/index.tsx create mode 100644 mobile-expo/src/components/tasks/TaskStatus/utils.ts create mode 100644 mobile-expo/src/utils/capitalizeString.ts create mode 100644 mobile-expo/src/utils/index.ts diff --git a/mobile-expo/src/components/common/CustomHeader/index.tsx b/mobile-expo/src/components/common/CustomHeader/index.tsx index f8d9b83..d4f96c5 100644 --- a/mobile-expo/src/components/common/CustomHeader/index.tsx +++ b/mobile-expo/src/components/common/CustomHeader/index.tsx @@ -1,4 +1,4 @@ -import { View, Text } from 'react-native'; +import { View } from 'react-native'; import { useSafeAreaInsets } from 'react-native-safe-area-context'; import { CustomHeaderButton, diff --git a/mobile-expo/src/components/common/CustomHeader/ui/CustomHeaderButton.tsx b/mobile-expo/src/components/common/CustomHeader/ui/CustomHeaderButton.tsx index 1fe0255..db16db9 100644 --- a/mobile-expo/src/components/common/CustomHeader/ui/CustomHeaderButton.tsx +++ b/mobile-expo/src/components/common/CustomHeader/ui/CustomHeaderButton.tsx @@ -3,6 +3,7 @@ import type { ComponentProps } from 'react'; import { TouchableOpacity } from 'react-native'; type IoniconsName = ComponentProps['name']; + export type TCustomHeaderButton = { name: IoniconsName; onPress?: () => void; diff --git a/mobile-expo/src/components/common/UIComponents/UIBox/index.tsx b/mobile-expo/src/components/common/UIComponents/UIBox/index.tsx index f13cdb6..4b29dd8 100644 --- a/mobile-expo/src/components/common/UIComponents/UIBox/index.tsx +++ b/mobile-expo/src/components/common/UIComponents/UIBox/index.tsx @@ -22,5 +22,6 @@ const styles = StyleSheet.create({ container: { padding: spacing.md, borderRadius: spacing.md, + gap: spacing.md, }, }); diff --git a/mobile-expo/src/components/tasks/TaskBlocked/index.tsx b/mobile-expo/src/components/tasks/TaskBlocked/index.tsx new file mode 100644 index 0000000..c41a042 --- /dev/null +++ b/mobile-expo/src/components/tasks/TaskBlocked/index.tsx @@ -0,0 +1,16 @@ +import { useTheme } from '@/hooks'; +import { Octicons } from '@expo/vector-icons'; +import { FC } from 'react'; +import { View } from 'react-native'; + +interface TaskBlockedProps {} + +export const TaskBlocked: FC = ({}) => { + const { theme } = useTheme(); + + return ( + + + + ); +}; diff --git a/mobile-expo/src/components/tasks/TaskCard/index.tsx b/mobile-expo/src/components/tasks/TaskCard/index.tsx index 33c0318..7e715b0 100644 --- a/mobile-expo/src/components/tasks/TaskCard/index.tsx +++ b/mobile-expo/src/components/tasks/TaskCard/index.tsx @@ -1,6 +1,11 @@ import { FC } from 'react'; import { UIBox, UIText } from '../../common'; import { TTask } from '@/types'; +import { TaskPriority } from '../TaskPriority'; +import { StyleSheet, View } from 'react-native'; +import { spacing } from '@/styles'; +import { TaskStatus } from '../TaskStatus'; +import { TaskBlocked } from '../TaskBlocked'; interface TaskCardProps { task: TTask; @@ -12,6 +17,19 @@ export const TaskCard: FC = ({ task }) => { return ( + + + + {isBlocked && } + ); }; + +const styles = StyleSheet.create({ + container: {}, + states: { + flexDirection: 'row', + gap: spacing.lg, + }, +}); diff --git a/mobile-expo/src/components/tasks/TaskPriority/index.tsx b/mobile-expo/src/components/tasks/TaskPriority/index.tsx new file mode 100644 index 0000000..45a7696 --- /dev/null +++ b/mobile-expo/src/components/tasks/TaskPriority/index.tsx @@ -0,0 +1,37 @@ +import { EnumTaskPriority } from '@/types'; +import { FC } from 'react'; +import { StyleSheet, View } from 'react-native'; +import { getTaskPriorityIcon } from './utils'; +import { useTheme } from '@/hooks'; +import { UIText } from '@/components/common'; +import FontAwesome5 from '@expo/vector-icons/FontAwesome5'; +import { spacing } from '@/styles'; +import { capitalizeString } from '@/utils'; + +interface TaskPriorityProps { + priority: EnumTaskPriority; + isLabelShown?: boolean; +} + +export const TaskPriority: FC = ({ + priority, + isLabelShown = true, +}) => { + const { theme } = useTheme(); + const { name, color } = getTaskPriorityIcon(priority, theme); + + return ( + + + {isLabelShown && } + + ); +}; + +const styles = StyleSheet.create({ + container: { + flexDirection: 'row', + alignItems: 'center', + gap: spacing.sm, + }, +}); diff --git a/mobile-expo/src/components/tasks/TaskPriority/utils.ts b/mobile-expo/src/components/tasks/TaskPriority/utils.ts new file mode 100644 index 0000000..d434a23 --- /dev/null +++ b/mobile-expo/src/components/tasks/TaskPriority/utils.ts @@ -0,0 +1,29 @@ +import { EnumTaskPriority, Theme } from '@/types'; + +export const getTaskPriorityIcon = ( + priority: EnumTaskPriority, + theme: Theme +) => { + switch (priority) { + case EnumTaskPriority.HIGH: + return { + name: 'angle-double-up', + color: theme.colors.priority.high, + }; + case EnumTaskPriority.MEDIUM: + return { + name: 'angle-up', + color: theme.colors.priority.medium, + }; + case EnumTaskPriority.LOW: + return { + name: 'angle-down', + color: theme.colors.priority.low, + }; + default: + return { + name: 'question', + color: theme.colors.priority.medium, + }; + } +}; diff --git a/mobile-expo/src/components/tasks/TaskStatus/index.tsx b/mobile-expo/src/components/tasks/TaskStatus/index.tsx new file mode 100644 index 0000000..2c51bec --- /dev/null +++ b/mobile-expo/src/components/tasks/TaskStatus/index.tsx @@ -0,0 +1,34 @@ +import { UIText } from '@/components/common'; +import { spacing } from '@/styles'; +import { EnumTaskStatus } from '@/types'; +import { capitalizeString } from '@/utils'; +import { FC } from 'react'; +import { StyleSheet, View } from 'react-native'; +import { getTaskStatusIcon } from './utils'; +import { useTheme } from '@/hooks'; +import { Entypo } from '@expo/vector-icons'; + +interface TaskStatusProps { + status: EnumTaskStatus; + isLabelShown?: boolean; +} + +export const TaskStatus: FC = ({ status, isLabelShown = true }) => { + const { theme } = useTheme(); + const { name, color } = getTaskStatusIcon(status, theme); + + return ( + + + {isLabelShown && } + + ); +}; + +const styles = StyleSheet.create({ + container: { + flexDirection: 'row', + alignItems: 'center', + gap: spacing.sm, + }, +}); diff --git a/mobile-expo/src/components/tasks/TaskStatus/utils.ts b/mobile-expo/src/components/tasks/TaskStatus/utils.ts new file mode 100644 index 0000000..61caa8d --- /dev/null +++ b/mobile-expo/src/components/tasks/TaskStatus/utils.ts @@ -0,0 +1,26 @@ +import { EnumTaskStatus, Theme } from '@/types'; + +export const getTaskStatusIcon = (status: EnumTaskStatus, theme: Theme) => { + switch (status) { + case EnumTaskStatus.TODO: + return { + name: 'progress-one', + color: theme.colors.status.todo, + }; + case EnumTaskStatus.IN_PROGRESS: + return { + name: 'progress-two', + color: theme.colors.status.inProgress, + }; + case EnumTaskStatus.DONE: + return { + name: 'progress-full', + color: theme.colors.status.done, + }; + default: + return { + name: 'progress-one', + color: theme.colors.status.inProgress, + }; + } +}; diff --git a/mobile-expo/src/components/tasks/index.ts b/mobile-expo/src/components/tasks/index.ts index 8f6e91d..424ed01 100644 --- a/mobile-expo/src/components/tasks/index.ts +++ b/mobile-expo/src/components/tasks/index.ts @@ -1 +1,4 @@ export * from './TaskCard'; +export * from './TaskPriority'; +export * from './TaskStatus'; +export * from './TaskBlocked'; diff --git a/mobile-expo/src/styles/theme/dark.ts b/mobile-expo/src/styles/theme/dark.ts index 01e597d..1c819a4 100644 --- a/mobile-expo/src/styles/theme/dark.ts +++ b/mobile-expo/src/styles/theme/dark.ts @@ -4,8 +4,8 @@ export const darkTheme: Theme = { colors: { base: { primary: '#BB86FC', - primaryDark: '#3700B3', - primaryLight: '#6200EE', + primaryDark: '#9845fe', + primaryLight: '#e1ccfc', }, text: { primary: '#FFFFFF', diff --git a/mobile-expo/src/styles/theme/light.ts b/mobile-expo/src/styles/theme/light.ts index 4c202b7..ff0ad53 100644 --- a/mobile-expo/src/styles/theme/light.ts +++ b/mobile-expo/src/styles/theme/light.ts @@ -5,7 +5,7 @@ export const lightTheme: Theme = { base: { primary: '#6200EE', primaryDark: '#3700B3', - primaryLight: '#BB86FC', + primaryLight: '#ae74ff', }, text: { primary: '#000000', diff --git a/mobile-expo/src/utils/capitalizeString.ts b/mobile-expo/src/utils/capitalizeString.ts new file mode 100644 index 0000000..fc6b313 --- /dev/null +++ b/mobile-expo/src/utils/capitalizeString.ts @@ -0,0 +1,3 @@ +export const capitalizeString = (str: string): string => { + return String(str).charAt(0).toUpperCase() + String(str).slice(1); +}; diff --git a/mobile-expo/src/utils/index.ts b/mobile-expo/src/utils/index.ts new file mode 100644 index 0000000..bbb6d13 --- /dev/null +++ b/mobile-expo/src/utils/index.ts @@ -0,0 +1 @@ +export * from './capitalizeString';