diff --git a/.vscode/settings.json b/.vscode/settings.json index 9d5b612..519cb58 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,4 +1,5 @@ { "postman.settings.dotenv-detection-notification-visibility": false, - "java.compile.nullAnalysis.mode": "automatic" + "java.compile.nullAnalysis.mode": "automatic", + "java.configuration.updateBuildConfiguration": "interactive" } \ No newline at end of file diff --git a/App.tsx b/App.tsx index 5676491..dfb5517 100644 --- a/App.tsx +++ b/App.tsx @@ -1,7 +1,8 @@ import React, { useEffect } from 'react'; -import { StyleSheet, useColorScheme } from 'react-native'; +import { StyleSheet } from 'react-native'; import { SafeAreaProvider } from 'react-native-safe-area-context'; import { PaperProvider } from 'react-native-paper'; +import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; import AppNavigator from './src/navigation/AppNavigator'; import notifee from '@notifee/react-native'; @@ -15,8 +16,16 @@ import { } from './src/utils/firebase/firebaseServices'; -import { dark, light } from './src/theme/theme'; -import { ThemeProvider} from './src/theme/ThemeProvider'; +import { ThemeProvider } from './src/theme/ThemeProvider'; + +const queryClient = new QueryClient({ + defaultOptions: { + queries: { + retry: 2, + staleTime: 1000 * 60 * 5, // 5 minutes + }, + }, +}); function App() { @@ -46,11 +55,13 @@ function App() { return ( - - - - - + + + + + + + ); } diff --git a/android/.settings/org.eclipse.buildship.core.prefs b/android/.settings/org.eclipse.buildship.core.prefs new file mode 100644 index 0000000..e479558 --- /dev/null +++ b/android/.settings/org.eclipse.buildship.core.prefs @@ -0,0 +1,13 @@ +arguments= +auto.sync=false +build.scans.enabled=false +connection.gradle.distribution=GRADLE_DISTRIBUTION(WRAPPER) +connection.project.dir= +eclipse.preferences.version=1 +gradle.user.home= +java.home= +jvm.arguments= +offline.mode=false +override.workspace.settings=false +show.console.view=false +show.executions.view=false diff --git a/android/app/build.gradle b/android/app/build.gradle index c741a4f..8230a48 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -117,4 +117,5 @@ dependencies { implementation jscFlavor } } -apply plugin: 'com.google.gms.google-services' \ No newline at end of file +apply plugin: 'com.google.gms.google-services' +apply from: "../../node_modules/react-native-vector-icons/fonts.gradle" \ No newline at end of file diff --git a/android/app/release/app-release.apk b/android/app/release/app-release.apk deleted file mode 100644 index 19b281a..0000000 Binary files a/android/app/release/app-release.apk and /dev/null differ diff --git a/android/app/release/baselineProfiles/0/app-release.dm b/android/app/release/baselineProfiles/0/app-release.dm deleted file mode 100644 index 17e7a8d..0000000 Binary files a/android/app/release/baselineProfiles/0/app-release.dm and /dev/null differ diff --git a/android/app/release/baselineProfiles/1/app-release.dm b/android/app/release/baselineProfiles/1/app-release.dm deleted file mode 100644 index eb636ce..0000000 Binary files a/android/app/release/baselineProfiles/1/app-release.dm and /dev/null differ diff --git a/android/app/release/output-metadata.json b/android/app/release/output-metadata.json deleted file mode 100644 index 08b0ec4..0000000 --- a/android/app/release/output-metadata.json +++ /dev/null @@ -1,37 +0,0 @@ -{ - "version": 3, - "artifactType": { - "type": "APK", - "kind": "Directory" - }, - "applicationId": "com.circlecollect", - "variantName": "release", - "elements": [ - { - "type": "SINGLE", - "filters": [], - "attributes": [], - "versionCode": 1, - "versionName": "1.0", - "outputFile": "app-release.apk" - } - ], - "elementType": "File", - "baselineProfiles": [ - { - "minApi": 28, - "maxApi": 30, - "baselineProfiles": [ - "baselineProfiles/1/app-release.dm" - ] - }, - { - "minApi": 31, - "maxApi": 2147483647, - "baselineProfiles": [ - "baselineProfiles/0/app-release.dm" - ] - } - ], - "minSdkVersionForDexing": 24 -} \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 0a11ef5..d0d9fbe 100644 --- a/package-lock.json +++ b/package-lock.json @@ -26,9 +26,11 @@ "@react-navigation/native-stack": "^7.3.26", "@react-navigation/stack": "^7.4.8", "@reduxjs/toolkit": "^2.9.0", + "@tanstack/react-query": "^5.90.21", "@types/react-native-material-ripple": "^0.9.6", "axios": "^1.12.2", "install": "^0.13.0", + "lucide-react-native": "^0.563.0", "npm": "^11.6.0", "react": "19.1.0", "react-native": "0.81.1", @@ -59,9 +61,9 @@ "@babel/core": "^7.25.2", "@babel/preset-env": "^7.25.3", "@babel/runtime": "^7.25.0", - "@react-native-community/cli": "20.0.0", - "@react-native-community/cli-platform-android": "20.0.0", - "@react-native-community/cli-platform-ios": "20.0.0", + "@react-native-community/cli": "^14.0.1", + "@react-native-community/cli-platform-android": "^15.1.0", + "@react-native-community/cli-platform-ios": "^14.0.1", "@react-native/babel-preset": "0.81.1", "@react-native/eslint-config": "0.81.1", "@react-native/metro-config": "0.81.1", @@ -2713,11 +2715,15 @@ }, "node_modules/@hapi/hoek": { "version": "9.3.0", + "resolved": "https://registry.npmjs.org/@hapi/hoek/-/hoek-9.3.0.tgz", + "integrity": "sha512-/c6rf4UJlmHlC9b5BaNvzAcFv7HZ2QHaV0D4/HNlBdvFnvQq8RI4kYdhyPCl7Xj+oWvTWQ8ujhqS53LIgAe6KQ==", "devOptional": true, "license": "BSD-3-Clause" }, "node_modules/@hapi/topo": { "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@hapi/topo/-/topo-5.1.0.tgz", + "integrity": "sha512-foQZKJig7Ob0BMAYBfcJk8d77QtOe7Wo4ox7ff1lQYoNNAb6jwcY1ncdoy2e9wQZzvNy7ODZCYJkK8kzmcAnAg==", "devOptional": true, "license": "BSD-3-Clause", "dependencies": { @@ -3342,16 +3348,19 @@ } }, "node_modules/@react-native-community/cli": { - "version": "20.0.0", + "version": "14.0.1", + "resolved": "https://registry.npmjs.org/@react-native-community/cli/-/cli-14.0.1.tgz", + "integrity": "sha512-QxBbQmZhhDZKOGTIjPr0cDrFcVrxlzFG1BmOuhok3x4xUk09B3zyIl8xnaqQ53YIVhXQHS0BX0f0evuKBcbcqQ==", "devOptional": true, "license": "MIT", "dependencies": { - "@react-native-community/cli-clean": "20.0.0", - "@react-native-community/cli-config": "20.0.0", - "@react-native-community/cli-doctor": "20.0.0", - "@react-native-community/cli-server-api": "20.0.0", - "@react-native-community/cli-tools": "20.0.0", - "@react-native-community/cli-types": "20.0.0", + "@react-native-community/cli-clean": "14.0.1", + "@react-native-community/cli-config": "14.0.1", + "@react-native-community/cli-debugger-ui": "14.0.1", + "@react-native-community/cli-doctor": "14.0.1", + "@react-native-community/cli-server-api": "14.0.1", + "@react-native-community/cli-tools": "14.0.1", + "@react-native-community/cli-types": "14.0.1", "chalk": "^4.1.2", "commander": "^9.4.1", "deepmerge": "^4.3.0", @@ -3370,22 +3379,26 @@ } }, "node_modules/@react-native-community/cli-clean": { - "version": "20.0.0", + "version": "14.0.1", + "resolved": "https://registry.npmjs.org/@react-native-community/cli-clean/-/cli-clean-14.0.1.tgz", + "integrity": "sha512-vf6dEwq0WmsQu2BViSZI8ascTdk7J4o82FRqpHiYSZkQau6eRmYFrgLoC5hg985Hp/HD+3wRSDW+lB/2+fz3yA==", "devOptional": true, "license": "MIT", "dependencies": { - "@react-native-community/cli-tools": "20.0.0", + "@react-native-community/cli-tools": "14.0.1", "chalk": "^4.1.2", "execa": "^5.0.0", "fast-glob": "^3.3.2" } }, "node_modules/@react-native-community/cli-config": { - "version": "20.0.0", + "version": "14.0.1", + "resolved": "https://registry.npmjs.org/@react-native-community/cli-config/-/cli-config-14.0.1.tgz", + "integrity": "sha512-yiyBdrsqZmaTtb5XFcfCwFavPagw7UxQVKnRBNVdwQNQHsRqhhPWZ0c7W+sESTMC/L9T0zvr7k4dxZ6TJEqqKQ==", "devOptional": true, "license": "MIT", "dependencies": { - "@react-native-community/cli-tools": "20.0.0", + "@react-native-community/cli-tools": "14.0.1", "chalk": "^4.1.2", "cosmiconfig": "^9.0.0", "deepmerge": "^4.3.0", @@ -3393,38 +3406,28 @@ "joi": "^17.2.1" } }, - "node_modules/@react-native-community/cli-config-android": { - "version": "20.0.0", + "node_modules/@react-native-community/cli-debugger-ui": { + "version": "14.0.1", + "resolved": "https://registry.npmjs.org/@react-native-community/cli-debugger-ui/-/cli-debugger-ui-14.0.1.tgz", + "integrity": "sha512-8eQ1U21Uwwm71jmIi9iaiou2VxqJQTm9k7Ch5Nj1ZEErO+nKFc9qExPTGtzsE80mHncLq8yNkQfV5waJvvhclg==", "devOptional": true, "license": "MIT", "dependencies": { - "@react-native-community/cli-tools": "20.0.0", - "chalk": "^4.1.2", - "fast-glob": "^3.3.2", - "fast-xml-parser": "^4.4.1" - } - }, - "node_modules/@react-native-community/cli-config-apple": { - "version": "20.0.0", - "devOptional": true, - "license": "MIT", - "dependencies": { - "@react-native-community/cli-tools": "20.0.0", - "chalk": "^4.1.2", - "execa": "^5.0.0", - "fast-glob": "^3.3.2" + "serve-static": "^1.13.1" } }, "node_modules/@react-native-community/cli-doctor": { - "version": "20.0.0", + "version": "14.0.1", + "resolved": "https://registry.npmjs.org/@react-native-community/cli-doctor/-/cli-doctor-14.0.1.tgz", + "integrity": "sha512-wXLa4cPgNuFJE6K2dv/0JRq62iJUiSVLN0Wa5TxugX+ojXBle/oe1W3nibOJFYdyRIEUGk3u7A6gdoznNtZEJg==", "devOptional": true, "license": "MIT", "dependencies": { - "@react-native-community/cli-config": "20.0.0", - "@react-native-community/cli-platform-android": "20.0.0", - "@react-native-community/cli-platform-apple": "20.0.0", - "@react-native-community/cli-platform-ios": "20.0.0", - "@react-native-community/cli-tools": "20.0.0", + "@react-native-community/cli-config": "14.0.1", + "@react-native-community/cli-platform-android": "14.0.1", + "@react-native-community/cli-platform-apple": "14.0.1", + "@react-native-community/cli-platform-ios": "14.0.1", + "@react-native-community/cli-tools": "14.0.1", "chalk": "^4.1.2", "command-exists": "^1.2.8", "deepmerge": "^4.3.0", @@ -3433,12 +3436,40 @@ "node-stream-zip": "^1.9.1", "ora": "^5.4.1", "semver": "^7.5.2", + "strip-ansi": "^5.2.0", "wcwidth": "^1.0.1", "yaml": "^2.2.1" } }, + "node_modules/@react-native-community/cli-doctor/node_modules/@react-native-community/cli-platform-android": { + "version": "14.0.1", + "resolved": "https://registry.npmjs.org/@react-native-community/cli-platform-android/-/cli-platform-android-14.0.1.tgz", + "integrity": "sha512-HNLIuD3ie8p1cLhsvfH19XPhjZjqZGX+zYlm4lpOKxPTSEzkaiAdOIMj5dVdN79zO1tmEawyyrwT4Jx5ddeUsQ==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@react-native-community/cli-tools": "14.0.1", + "chalk": "^4.1.2", + "execa": "^5.0.0", + "fast-glob": "^3.3.2", + "fast-xml-parser": "^4.2.4", + "logkitty": "^0.7.1" + } + }, + "node_modules/@react-native-community/cli-doctor/node_modules/ansi-regex": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.1.tgz", + "integrity": "sha512-ILlv4k/3f6vfQ4OoP2AGvirOktlQ98ZEL1k9FaQjxa3L1abBgbuTDAdPOpvbGncC0BTVQrl+OM8xZGK6tWXt7g==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/@react-native-community/cli-doctor/node_modules/semver": { - "version": "7.7.2", + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", "devOptional": true, "license": "ISC", "bin": { @@ -3448,74 +3479,183 @@ "node": ">=10" } }, - "node_modules/@react-native-community/cli-platform-android": { - "version": "20.0.0", + "node_modules/@react-native-community/cli-doctor/node_modules/strip-ansi": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", + "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", "devOptional": true, "license": "MIT", "dependencies": { - "@react-native-community/cli-config-android": "20.0.0", - "@react-native-community/cli-tools": "20.0.0", + "ansi-regex": "^4.1.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/@react-native-community/cli-platform-android": { + "version": "15.1.0", + "resolved": "https://registry.npmjs.org/@react-native-community/cli-platform-android/-/cli-platform-android-15.1.0.tgz", + "integrity": "sha512-7gKM/3RK0BVyWEzUTQMvAMUvdvX/IQdKjaOjEgi94lVmRrQiCMhcP+eM3nl1ds7GpoXAGZjHC9j7z1yfDYSMmQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@react-native-community/cli-tools": "15.1.0", "chalk": "^4.1.2", "execa": "^5.0.0", + "fast-glob": "^3.3.2", + "fast-xml-parser": "^4.4.1", "logkitty": "^0.7.1" } }, - "node_modules/@react-native-community/cli-platform-apple": { - "version": "20.0.0", - "devOptional": true, + "node_modules/@react-native-community/cli-platform-android/node_modules/@react-native-community/cli-tools": { + "version": "15.1.0", + "resolved": "https://registry.npmjs.org/@react-native-community/cli-tools/-/cli-tools-15.1.0.tgz", + "integrity": "sha512-WucpEF33q3sjlhRTww8awrmq6k5AqwFOVQtk/iY4sWMwiJDvhRh0+zzh8rTt9LN/cLh+Lvyq+SOuSWFW7emjug==", + "dev": true, "license": "MIT", "dependencies": { - "@react-native-community/cli-config-apple": "20.0.0", - "@react-native-community/cli-tools": "20.0.0", - "chalk": "^4.1.2", - "execa": "^5.0.0", - "fast-xml-parser": "^4.4.1" - } - }, - "node_modules/@react-native-community/cli-platform-ios": { - "version": "20.0.0", - "devOptional": true, - "license": "MIT", - "dependencies": { - "@react-native-community/cli-platform-apple": "20.0.0" - } - }, - "node_modules/@react-native-community/cli-server-api": { - "version": "20.0.0", - "devOptional": true, - "license": "MIT", - "dependencies": { - "@react-native-community/cli-tools": "20.0.0", - "body-parser": "^1.20.3", - "compression": "^1.7.1", - "connect": "^3.6.5", - "errorhandler": "^1.5.1", - "nocache": "^3.0.1", - "open": "^6.2.0", - "pretty-format": "^29.7.0", - "serve-static": "^1.13.1", - "ws": "^6.2.3" - } - }, - "node_modules/@react-native-community/cli-tools": { - "version": "20.0.0", - "devOptional": true, - "license": "MIT", - "dependencies": { - "@vscode/sudo-prompt": "^9.0.0", "appdirsjs": "^1.2.4", "chalk": "^4.1.2", "execa": "^5.0.0", "find-up": "^5.0.0", - "launch-editor": "^2.9.1", "mime": "^2.4.1", + "open": "^6.2.0", "ora": "^5.4.1", "prompts": "^2.4.2", - "semver": "^7.5.2" + "semver": "^7.5.2", + "shell-quote": "^1.7.3", + "sudo-prompt": "^9.0.0" + } + }, + "node_modules/@react-native-community/cli-platform-android/node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@react-native-community/cli-platform-apple": { + "version": "14.0.1", + "resolved": "https://registry.npmjs.org/@react-native-community/cli-platform-apple/-/cli-platform-apple-14.0.1.tgz", + "integrity": "sha512-KY7zlDpzKfVzBuHWde7EXKgdkFFYl5Lp1o1ro8ie83sDzwSVTSyd5Pmb351SJABJuNFSuTzbyyMiuG+1BQ4hyg==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@react-native-community/cli-tools": "14.0.1", + "chalk": "^4.1.2", + "execa": "^5.0.0", + "fast-glob": "^3.3.2", + "fast-xml-parser": "^4.2.4", + "ora": "^5.4.1" + } + }, + "node_modules/@react-native-community/cli-platform-ios": { + "version": "14.0.1", + "resolved": "https://registry.npmjs.org/@react-native-community/cli-platform-ios/-/cli-platform-ios-14.0.1.tgz", + "integrity": "sha512-9DKbMHf53A4ae2sJ0rZFqi2m7Z1NoaWZYhkWufO8tEXCKckBat4ddio/Ei6g0bQk8IKdLzHogQFMuU3jbT3LYA==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@react-native-community/cli-platform-apple": "14.0.1" + } + }, + "node_modules/@react-native-community/cli-server-api": { + "version": "14.0.1", + "resolved": "https://registry.npmjs.org/@react-native-community/cli-server-api/-/cli-server-api-14.0.1.tgz", + "integrity": "sha512-8oKMcDH72vuX2pWv+N07NcBWoMOykK7cUKshCjSqupWNr1wGxHnJC4uFvLLAwpoewYlOQXMnU9Jn/9IWA6Sm2w==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@react-native-community/cli-debugger-ui": "14.0.1", + "@react-native-community/cli-tools": "14.0.1", + "compression": "^1.7.1", + "connect": "^3.6.5", + "errorhandler": "^1.5.1", + "nocache": "^3.0.1", + "pretty-format": "^26.6.2", + "serve-static": "^1.13.1", + "ws": "^6.2.3" + } + }, + "node_modules/@react-native-community/cli-server-api/node_modules/@jest/types": { + "version": "26.6.2", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-26.6.2.tgz", + "integrity": "sha512-fC6QCp7Sc5sX6g8Tvbmj4XUTbyrik0akgRy03yjXbQaBWWNWGE7SGtJk98m0N8nzegD/7SggrUlivxo5ax4KWQ==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^3.0.0", + "@types/node": "*", + "@types/yargs": "^15.0.0", + "chalk": "^4.0.0" + }, + "engines": { + "node": ">= 10.14.2" + } + }, + "node_modules/@react-native-community/cli-server-api/node_modules/@types/yargs": { + "version": "15.0.20", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-15.0.20.tgz", + "integrity": "sha512-KIkX+/GgfFitlASYCGoSF+T4XRXhOubJLhkLVtSfsRTe9jWMmuM2g28zQ41BtPTG7TRBb2xHW+LCNVE9QR/vsg==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@types/yargs-parser": "*" + } + }, + "node_modules/@react-native-community/cli-server-api/node_modules/pretty-format": { + "version": "26.6.2", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-26.6.2.tgz", + "integrity": "sha512-7AeGuCYNGmycyQbCqd/3PWH4eOoX/OiCa0uphp57NVTeAGdJGaAliecxwBDHYQCIvrW7aDBZCYeNTP/WX69mkg==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^26.6.2", + "ansi-regex": "^5.0.0", + "ansi-styles": "^4.0.0", + "react-is": "^17.0.1" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/@react-native-community/cli-server-api/node_modules/react-is": { + "version": "17.0.2", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", + "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/@react-native-community/cli-tools": { + "version": "14.0.1", + "resolved": "https://registry.npmjs.org/@react-native-community/cli-tools/-/cli-tools-14.0.1.tgz", + "integrity": "sha512-4tksgruPliZlQmO6kfX1gUBNvDChoOcZgGt9r4KvvyFelCR3VUWEoJkvhKlDlSoaEtzBO2L426CTnFK4ED/yBQ==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "appdirsjs": "^1.2.4", + "chalk": "^4.1.2", + "execa": "^5.0.0", + "find-up": "^5.0.0", + "mime": "^2.4.1", + "open": "^6.2.0", + "ora": "^5.4.1", + "semver": "^7.5.2", + "shell-quote": "^1.7.3", + "sudo-prompt": "^9.0.0" } }, "node_modules/@react-native-community/cli-tools/node_modules/semver": { - "version": "7.7.2", + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", "devOptional": true, "license": "ISC", "bin": { @@ -3526,7 +3666,9 @@ } }, "node_modules/@react-native-community/cli-types": { - "version": "20.0.0", + "version": "14.0.1", + "resolved": "https://registry.npmjs.org/@react-native-community/cli-types/-/cli-types-14.0.1.tgz", + "integrity": "sha512-Yk6TLdkSQHaSG9NlcMsHtX7ZYuHaOrZ7K/3zBbpabWUm006blND5NKw8YQuk9guqK7i/PMSUKJUq1NbF7bGjDA==", "devOptional": true, "license": "MIT", "dependencies": { @@ -4195,6 +4337,8 @@ }, "node_modules/@sideway/address": { "version": "4.1.5", + "resolved": "https://registry.npmjs.org/@sideway/address/-/address-4.1.5.tgz", + "integrity": "sha512-IqO/DUQHUkPeixNQ8n0JA6102hT9CmaljNTPmQ1u8MEhBo/R4Q8eKLN/vGZxuebwOroDB4cbpjheD4+/sKFK4Q==", "devOptional": true, "license": "BSD-3-Clause", "dependencies": { @@ -4203,11 +4347,15 @@ }, "node_modules/@sideway/formula": { "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@sideway/formula/-/formula-3.0.1.tgz", + "integrity": "sha512-/poHZJJVjx3L+zVD6g9KgHfYnb443oi7wLu/XKojDviHy6HOEOA6z1Trk5aR1dGcmPenJEgb2sK2I80LeS3MIg==", "devOptional": true, "license": "BSD-3-Clause" }, "node_modules/@sideway/pinpoint": { "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@sideway/pinpoint/-/pinpoint-2.0.0.tgz", + "integrity": "sha512-RNiOoTPkptFtSVzQevY/yWtZwf/RxyVnPy/OcA9HBM3MlGDnBEYL5B41H0MTn0Uec8Hi+2qUtTfG2WWZBmMejQ==", "devOptional": true, "license": "BSD-3-Clause" }, @@ -4237,6 +4385,32 @@ "version": "0.3.0", "license": "MIT" }, + "node_modules/@tanstack/query-core": { + "version": "5.90.20", + "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.90.20.tgz", + "integrity": "sha512-OMD2HLpNouXEfZJWcKeVKUgQ5n+n3A2JFmBaScpNDUqSrQSjiveC7dKMe53uJUg1nDG16ttFPz2xfilz6i2uVg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, + "node_modules/@tanstack/react-query": { + "version": "5.90.21", + "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.90.21.tgz", + "integrity": "sha512-0Lu6y5t+tvlTJMTO7oh5NSpJfpg/5D41LlThfepTixPYkJ0sE2Jj0m0f6yYqujBwIXlId87e234+MxG3D3g7kg==", + "license": "MIT", + "dependencies": { + "@tanstack/query-core": "5.90.20" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "react": "^18 || ^19" + } + }, "node_modules/@types/babel__core": { "version": "7.20.5", "license": "MIT", @@ -4589,11 +4763,6 @@ "dev": true, "license": "ISC" }, - "node_modules/@vscode/sudo-prompt": { - "version": "9.3.1", - "devOptional": true, - "license": "MIT" - }, "node_modules/@webgpu/types": { "version": "0.1.21", "resolved": "https://registry.npmjs.org/@webgpu/types/-/types-0.1.21.tgz", @@ -4707,6 +4876,8 @@ }, "node_modules/ansi-fragments": { "version": "0.2.1", + "resolved": "https://registry.npmjs.org/ansi-fragments/-/ansi-fragments-0.2.1.tgz", + "integrity": "sha512-DykbNHxuXQwUDRv5ibc2b0x7uw7wmwOGLBUd5RmaQ5z8Lhx19vwvKV+FAsM5rEA6dEcHxX+/Ad5s9eF2k2bB+w==", "devOptional": true, "license": "MIT", "dependencies": { @@ -4717,6 +4888,8 @@ }, "node_modules/ansi-fragments/node_modules/ansi-regex": { "version": "4.1.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.1.tgz", + "integrity": "sha512-ILlv4k/3f6vfQ4OoP2AGvirOktlQ98ZEL1k9FaQjxa3L1abBgbuTDAdPOpvbGncC0BTVQrl+OM8xZGK6tWXt7g==", "devOptional": true, "license": "MIT", "engines": { @@ -4725,6 +4898,8 @@ }, "node_modules/ansi-fragments/node_modules/strip-ansi": { "version": "5.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", + "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", "devOptional": true, "license": "MIT", "dependencies": { @@ -4767,6 +4942,8 @@ }, "node_modules/appdirsjs": { "version": "1.2.7", + "resolved": "https://registry.npmjs.org/appdirsjs/-/appdirsjs-1.2.7.tgz", + "integrity": "sha512-Quji6+8kLBC3NnBeo14nPDq0+2jUs5s3/xEye+udFHumHhRk4M7aAMXp/PBJqkKYGuuyR9M/6Dq7d2AViiGmhw==", "devOptional": true, "license": "MIT" }, @@ -4913,6 +5090,8 @@ }, "node_modules/astral-regex": { "version": "1.0.0", + "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-1.0.0.tgz", + "integrity": "sha512-+Ryf6g3BKoRc7jfp7ad8tM4TtMiaWvbF/1/sQcZPkkS7ag3D5nMBCe2UfOTONtAkaG0tO0ij3C5Lwmf1EiyjHg==", "devOptional": true, "license": "MIT", "engines": { @@ -4933,6 +5112,8 @@ }, "node_modules/asynckit": { "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", "license": "MIT" }, "node_modules/available-typed-arrays": { @@ -4950,11 +5131,13 @@ } }, "node_modules/axios": { - "version": "1.12.2", + "version": "1.13.5", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.13.5.tgz", + "integrity": "sha512-cz4ur7Vb0xS4/KUN0tPWe44eqxrIu31me+fbang3ijiNscE129POzipJJA6zniq2C/Z6sJCjMimjS8Lc/GAs8Q==", "license": "MIT", "dependencies": { - "follow-redirects": "^1.15.6", - "form-data": "^4.0.4", + "follow-redirects": "^1.15.11", + "form-data": "^4.0.5", "proxy-from-env": "^1.1.0" } }, @@ -5138,6 +5321,8 @@ }, "node_modules/bl": { "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", + "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", "devOptional": true, "license": "MIT", "dependencies": { @@ -5146,42 +5331,6 @@ "readable-stream": "^3.4.0" } }, - "node_modules/body-parser": { - "version": "1.20.3", - "devOptional": true, - "license": "MIT", - "dependencies": { - "bytes": "3.1.2", - "content-type": "~1.0.5", - "debug": "2.6.9", - "depd": "2.0.0", - "destroy": "1.2.0", - "http-errors": "2.0.0", - "iconv-lite": "0.4.24", - "on-finished": "2.4.1", - "qs": "6.13.0", - "raw-body": "2.5.2", - "type-is": "~1.6.18", - "unpipe": "1.0.0" - }, - "engines": { - "node": ">= 0.8", - "npm": "1.2.8000 || >= 1.4.16" - } - }, - "node_modules/body-parser/node_modules/debug": { - "version": "2.6.9", - "devOptional": true, - "license": "MIT", - "dependencies": { - "ms": "2.0.0" - } - }, - "node_modules/body-parser/node_modules/ms": { - "version": "2.0.0", - "devOptional": true, - "license": "MIT" - }, "node_modules/boolbase": { "version": "1.0.0", "license": "ISC" @@ -5244,6 +5393,8 @@ }, "node_modules/buffer": { "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", "devOptional": true, "funding": [ { @@ -5271,6 +5422,8 @@ }, "node_modules/bytes": { "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", "devOptional": true, "license": "MIT", "engines": { @@ -5307,7 +5460,7 @@ }, "node_modules/call-bound": { "version": "1.0.4", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "call-bind-apply-helpers": "^1.0.2", @@ -5453,6 +5606,8 @@ }, "node_modules/cli-cursor": { "version": "3.1.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz", + "integrity": "sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==", "devOptional": true, "license": "MIT", "dependencies": { @@ -5464,6 +5619,8 @@ }, "node_modules/cli-spinners": { "version": "2.9.2", + "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.9.2.tgz", + "integrity": "sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==", "devOptional": true, "license": "MIT", "engines": { @@ -5487,6 +5644,8 @@ }, "node_modules/clone": { "version": "1.0.4", + "resolved": "https://registry.npmjs.org/clone/-/clone-1.0.4.tgz", + "integrity": "sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==", "devOptional": true, "license": "MIT", "engines": { @@ -5542,11 +5701,15 @@ }, "node_modules/colorette": { "version": "1.4.0", + "resolved": "https://registry.npmjs.org/colorette/-/colorette-1.4.0.tgz", + "integrity": "sha512-Y2oEozpomLn7Q3HFP7dpww7AtMJplbM9lGZP6RDfHqmbeRjiwRg4n6VM6j4KLmRke85uWEI7JqF17f3pqdRA0g==", "devOptional": true, "license": "MIT" }, "node_modules/combined-stream": { "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", "license": "MIT", "dependencies": { "delayed-stream": "~1.0.0" @@ -5557,6 +5720,8 @@ }, "node_modules/command-exists": { "version": "1.2.9", + "resolved": "https://registry.npmjs.org/command-exists/-/command-exists-1.2.9.tgz", + "integrity": "sha512-LTQ/SGc+s0Xc0Fu5WaKnR0YiygZkm9eKFvyS+fRsU7/ZWFF8ykFM6Pc9aCVf1+xasOOZpO3BAVgVrKvsqKHV7w==", "devOptional": true, "license": "MIT" }, @@ -5570,6 +5735,8 @@ }, "node_modules/compressible": { "version": "2.0.18", + "resolved": "https://registry.npmjs.org/compressible/-/compressible-2.0.18.tgz", + "integrity": "sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg==", "devOptional": true, "license": "MIT", "dependencies": { @@ -5581,6 +5748,8 @@ }, "node_modules/compression": { "version": "1.8.1", + "resolved": "https://registry.npmjs.org/compression/-/compression-1.8.1.tgz", + "integrity": "sha512-9mAqGPHLakhCLeNyxPkK4xVo746zQ/czLH1Ky+vkitMnWfWZps8r0qXuwhwizagCRttsL4lfG4pIOvaWLpAP0w==", "devOptional": true, "license": "MIT", "dependencies": { @@ -5598,6 +5767,8 @@ }, "node_modules/compression/node_modules/debug": { "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", "devOptional": true, "license": "MIT", "dependencies": { @@ -5606,6 +5777,8 @@ }, "node_modules/compression/node_modules/ms": { "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", "devOptional": true, "license": "MIT" }, @@ -5637,14 +5810,6 @@ "version": "2.0.0", "license": "MIT" }, - "node_modules/content-type": { - "version": "1.0.5", - "devOptional": true, - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, "node_modules/convert-source-map": { "version": "2.0.0", "license": "MIT" @@ -5663,6 +5828,8 @@ }, "node_modules/cosmiconfig": { "version": "9.0.0", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-9.0.0.tgz", + "integrity": "sha512-itvL5h8RETACmOTFc4UfIyB2RfEHi71Ax6E/PivVxq9NseKbOWpeyHEOIbmAw1rs8Ak0VursQNww7lf7YtUwzg==", "devOptional": true, "license": "MIT", "dependencies": { @@ -5807,7 +5974,9 @@ } }, "node_modules/dayjs": { - "version": "1.11.18", + "version": "1.11.19", + "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.19.tgz", + "integrity": "sha512-t5EcLVS6QPBNqM2z8fakk/NKel+Xzshgt8FFKAn+qwlD1pzZWxh0nVCrvFK7ZDb6XucZeF9z8C7CBWTRIVApAw==", "devOptional": true, "license": "MIT" }, @@ -5828,6 +5997,8 @@ }, "node_modules/decamelize": { "version": "1.2.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", + "integrity": "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==", "devOptional": true, "license": "MIT", "engines": { @@ -5868,6 +6039,8 @@ }, "node_modules/defaults": { "version": "1.0.4", + "resolved": "https://registry.npmjs.org/defaults/-/defaults-1.0.4.tgz", + "integrity": "sha512-eFuaLoy/Rxalv2kr+lqMlUnrDWV+3j4pljOIJgLIhI058IQfWJ7vXhyEIHu+HtC738klGALYxOKDO0bQP3tg8A==", "devOptional": true, "license": "MIT", "dependencies": { @@ -5911,6 +6084,8 @@ }, "node_modules/delayed-stream": { "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", "license": "MIT", "engines": { "node": ">=0.4.0" @@ -6070,6 +6245,8 @@ }, "node_modules/env-paths": { "version": "2.2.1", + "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", + "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==", "devOptional": true, "license": "MIT", "engines": { @@ -6077,7 +6254,9 @@ } }, "node_modules/envinfo": { - "version": "7.14.0", + "version": "7.21.0", + "resolved": "https://registry.npmjs.org/envinfo/-/envinfo-7.21.0.tgz", + "integrity": "sha512-Lw7I8Zp5YKHFCXL7+Dz95g4CcbMEpgvqZNNq3AmlT5XAV6CgAAk6gyAMqn2zjw08K9BHfcNuKrMiCPLByGafow==", "devOptional": true, "license": "MIT", "bin": { @@ -6103,15 +6282,21 @@ } }, "node_modules/errorhandler": { - "version": "1.5.1", + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/errorhandler/-/errorhandler-1.5.2.tgz", + "integrity": "sha512-kNAL7hESndBCrWwS72QyV3IVOTrVmj9D062FV5BQswNL5zEdeRmz/WJFyh6Aj/plvvSOrzddkxW57HgkZcR9Fw==", "devOptional": true, "license": "MIT", "dependencies": { - "accepts": "~1.3.7", + "accepts": "~1.3.8", "escape-html": "~1.0.3" }, "engines": { "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, "node_modules/es-abstract": { @@ -6898,6 +7083,8 @@ }, "node_modules/fast-xml-parser": { "version": "4.5.3", + "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-4.5.3.tgz", + "integrity": "sha512-RKihhV+SHsIUGXObeVy9AXiBbFwkVk7Syp8XgwN5U3JV416+Gwp/GO9i0JYKmikykgz/UHRrrV4ROuZEo/T0ig==", "devOptional": true, "funding": [ { @@ -7111,7 +7298,9 @@ } }, "node_modules/form-data": { - "version": "4.0.4", + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", + "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", "license": "MIT", "dependencies": { "asynckit": "^0.4.0", @@ -7540,17 +7729,6 @@ "node": ">=10.17.0" } }, - "node_modules/iconv-lite": { - "version": "0.4.24", - "devOptional": true, - "license": "MIT", - "dependencies": { - "safer-buffer": ">= 2.1.2 < 3" - }, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/idb": { "version": "7.1.1", "resolved": "https://registry.npmjs.org/idb/-/idb-7.1.1.tgz", @@ -7559,6 +7737,8 @@ }, "node_modules/ieee754": { "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", "devOptional": true, "funding": [ { @@ -7845,6 +8025,8 @@ }, "node_modules/is-fullwidth-code-point": { "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", + "integrity": "sha512-VHskAKYM8RfSFXwee5t5cbN5PZeq1Wrh6qd5bkyiXIf6UQcN6w/A0eXM9r6t8d+GYOh+o6ZhiEnb88LN/Y8m2w==", "devOptional": true, "license": "MIT", "engines": { @@ -7889,6 +8071,8 @@ }, "node_modules/is-interactive": { "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-interactive/-/is-interactive-1.0.0.tgz", + "integrity": "sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w==", "devOptional": true, "license": "MIT", "engines": { @@ -8056,6 +8240,8 @@ }, "node_modules/is-unicode-supported": { "version": "0.1.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz", + "integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==", "devOptional": true, "license": "MIT", "engines": { @@ -8107,6 +8293,8 @@ }, "node_modules/is-wsl": { "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-1.1.0.tgz", + "integrity": "sha512-gfygJYZ2gLTDlmbWMI0CE2MwnFzSN/2SZfkMlItC4K/JBlsWVDB0bO6XhqcY13YXE7iMcAJnzTCJjPiTeJJ0Mw==", "devOptional": true, "license": "MIT", "engines": { @@ -8739,6 +8927,8 @@ }, "node_modules/joi": { "version": "17.13.3", + "resolved": "https://registry.npmjs.org/joi/-/joi-17.13.3.tgz", + "integrity": "sha512-otDA4ldcIx+ZXsKHWmp0YizCweVRZG96J10b0FevjfuncLO1oX59THoAmHkNubYJ+9gWsYsp5k8v4ib6oDv1fA==", "devOptional": true, "license": "BSD-3-Clause", "dependencies": { @@ -8848,15 +9038,6 @@ "node": ">=6" } }, - "node_modules/launch-editor": { - "version": "2.11.1", - "devOptional": true, - "license": "MIT", - "dependencies": { - "picocolors": "^1.1.1", - "shell-quote": "^1.8.3" - } - }, "node_modules/leven": { "version": "3.1.0", "license": "MIT", @@ -8915,7 +9096,9 @@ } }, "node_modules/lodash": { - "version": "4.17.21", + "version": "4.17.23", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.23.tgz", + "integrity": "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==", "license": "MIT" }, "node_modules/lodash.camelcase": { @@ -8943,6 +9126,8 @@ }, "node_modules/log-symbols": { "version": "4.1.0", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", + "integrity": "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==", "devOptional": true, "license": "MIT", "dependencies": { @@ -8958,6 +9143,8 @@ }, "node_modules/logkitty": { "version": "0.7.1", + "resolved": "https://registry.npmjs.org/logkitty/-/logkitty-0.7.1.tgz", + "integrity": "sha512-/3ER20CTTbahrCrpYfPn7Xavv9diBROZpoXGVZDWMw4b/X4uuUwAC0ki85tgsdMRONURyIJbcOvS94QsUBYPbQ==", "devOptional": true, "license": "MIT", "dependencies": { @@ -8971,6 +9158,8 @@ }, "node_modules/logkitty/node_modules/cliui": { "version": "6.0.0", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz", + "integrity": "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==", "devOptional": true, "license": "ISC", "dependencies": { @@ -8981,6 +9170,8 @@ }, "node_modules/logkitty/node_modules/find-up": { "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", "devOptional": true, "license": "MIT", "dependencies": { @@ -8993,6 +9184,8 @@ }, "node_modules/logkitty/node_modules/locate-path": { "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", "devOptional": true, "license": "MIT", "dependencies": { @@ -9004,6 +9197,8 @@ }, "node_modules/logkitty/node_modules/p-limit": { "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", "devOptional": true, "license": "MIT", "dependencies": { @@ -9018,6 +9213,8 @@ }, "node_modules/logkitty/node_modules/p-locate": { "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", "devOptional": true, "license": "MIT", "dependencies": { @@ -9029,6 +9226,8 @@ }, "node_modules/logkitty/node_modules/wrap-ansi": { "version": "6.2.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", + "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", "devOptional": true, "license": "MIT", "dependencies": { @@ -9042,11 +9241,15 @@ }, "node_modules/logkitty/node_modules/y18n": { "version": "4.0.3", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.3.tgz", + "integrity": "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==", "devOptional": true, "license": "ISC" }, "node_modules/logkitty/node_modules/yargs": { "version": "15.4.1", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-15.4.1.tgz", + "integrity": "sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==", "devOptional": true, "license": "MIT", "dependencies": { @@ -9068,6 +9271,8 @@ }, "node_modules/logkitty/node_modules/yargs-parser": { "version": "18.1.3", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.3.tgz", + "integrity": "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==", "devOptional": true, "license": "ISC", "dependencies": { @@ -9101,6 +9306,17 @@ "yallist": "^3.0.2" } }, + "node_modules/lucide-react-native": { + "version": "0.563.0", + "resolved": "https://registry.npmjs.org/lucide-react-native/-/lucide-react-native-0.563.0.tgz", + "integrity": "sha512-q4tYoAMorTqv+UXRYc0MyiEAOF+4Bu73zxD63EDrnGCFL+xuj+imBm3E2rIKRmME0heVHlK+98fsi8wbL92LNQ==", + "license": "ISC", + "peerDependencies": { + "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-native": "*", + "react-native-svg": "^12.0.0 || ^13.0.0 || ^14.0.0 || ^15.0.0" + } + }, "node_modules/make-dir": { "version": "4.0.0", "dev": true, @@ -9148,14 +9364,6 @@ "version": "2.0.14", "license": "CC0-1.0" }, - "node_modules/media-typer": { - "version": "0.3.0", - "devOptional": true, - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, "node_modules/memoize-one": { "version": "5.2.1", "license": "MIT" @@ -9504,6 +9712,8 @@ }, "node_modules/mime": { "version": "2.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-2.6.0.tgz", + "integrity": "sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==", "devOptional": true, "license": "MIT", "bin": { @@ -9515,6 +9725,8 @@ }, "node_modules/mime-db": { "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", "devOptional": true, "license": "MIT", "engines": { @@ -9605,6 +9817,8 @@ }, "node_modules/negotiator": { "version": "0.6.4", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.4.tgz", + "integrity": "sha512-myRT3DiWPHqho5PrJaIRyaMv2kgYf0mUVgBNOYMuCH5Ki1yEiQaf/ZJuQ62nvpc44wL5WDbTX7yGJi1Neevw8w==", "devOptional": true, "license": "MIT", "engines": { @@ -9613,6 +9827,8 @@ }, "node_modules/nocache": { "version": "3.0.4", + "resolved": "https://registry.npmjs.org/nocache/-/nocache-3.0.4.tgz", + "integrity": "sha512-WDD0bdg9mbq6F4mRxEYcPWwfA1vxd0mrvKOyxI7Xj/atfRHVeutzuWByG//jfm4uPzp0y4Kj051EORCBSQMycw==", "devOptional": true, "license": "MIT", "engines": { @@ -9629,6 +9845,8 @@ }, "node_modules/node-stream-zip": { "version": "1.15.0", + "resolved": "https://registry.npmjs.org/node-stream-zip/-/node-stream-zip-1.15.0.tgz", + "integrity": "sha512-LN4fydt9TqhZhThkZIVQnF9cwjU3qmUH9h78Mx/K7d3VvfRqqwthLwJEUOEL0QPZ0XQmNN7be5Ggit5+4dq3Bw==", "devOptional": true, "license": "MIT", "engines": { @@ -9647,9 +9865,9 @@ } }, "node_modules/npm": { - "version": "11.6.4", - "resolved": "https://registry.npmjs.org/npm/-/npm-11.6.4.tgz", - "integrity": "sha512-ERjKtGoFpQrua/9bG0+h3xiv/4nVdGViCjUYA1AmlV24fFvfnSB7B7dIfZnySQ1FDLd0ZVrWPsLLp78dCtJdRQ==", + "version": "11.9.0", + "resolved": "https://registry.npmjs.org/npm/-/npm-11.9.0.tgz", + "integrity": "sha512-BBZoU926FCypj4b7V7ElinxsWcy4Kss88UG3ejFYmKyq7Uc5XnT34Me2nEhgCOaL5qY4HvGu5aI92C4OYd7NaA==", "bundleDependencies": [ "@isaacs/string-locale-compare", "@npmcli/arborist", @@ -9728,8 +9946,8 @@ ], "dependencies": { "@isaacs/string-locale-compare": "^1.1.0", - "@npmcli/arborist": "^9.1.8", - "@npmcli/config": "^10.4.4", + "@npmcli/arborist": "^9.2.0", + "@npmcli/config": "^10.6.0", "@npmcli/fs": "^5.0.0", "@npmcli/map-workspaces": "^5.0.3", "@npmcli/metavuln-calculator": "^9.0.3", @@ -9737,12 +9955,12 @@ "@npmcli/promise-spawn": "^9.0.1", "@npmcli/redact": "^4.0.0", "@npmcli/run-script": "^10.0.3", - "@sigstore/tuf": "^4.0.0", + "@sigstore/tuf": "^4.0.1", "abbrev": "^4.0.0", "archy": "~1.0.0", "cacache": "^20.0.3", "chalk": "^5.6.2", - "ci-info": "^4.3.1", + "ci-info": "^4.4.0", "cli-columns": "^4.0.0", "fastest-levenshtein": "^1.0.16", "fs-minipass": "^3.0.3", @@ -9754,11 +9972,11 @@ "is-cidr": "^6.0.1", "json-parse-even-better-errors": "^5.0.0", "libnpmaccess": "^10.0.3", - "libnpmdiff": "^8.0.11", - "libnpmexec": "^10.1.10", - "libnpmfund": "^7.0.11", + "libnpmdiff": "^8.1.0", + "libnpmexec": "^10.2.0", + "libnpmfund": "^7.0.14", "libnpmorg": "^8.0.1", - "libnpmpack": "^9.0.11", + "libnpmpack": "^9.1.0", "libnpmpublish": "^11.1.3", "libnpmsearch": "^9.0.1", "libnpmteam": "^8.0.2", @@ -9768,7 +9986,7 @@ "minipass": "^7.1.1", "minipass-pipeline": "^1.2.4", "ms": "^2.1.2", - "node-gyp": "^12.1.0", + "node-gyp": "^12.2.0", "nopt": "^9.0.0", "npm-audit-report": "^7.0.0", "npm-install-checks": "^8.0.0", @@ -9778,7 +9996,7 @@ "npm-registry-fetch": "^19.1.1", "npm-user-validate": "^4.0.0", "p-map": "^7.0.4", - "pacote": "^21.0.4", + "pacote": "^21.1.0", "parse-conflict-json": "^5.0.1", "proc-log": "^6.1.0", "qrcode-terminal": "^0.12.0", @@ -9787,11 +10005,11 @@ "spdx-expression-parse": "^4.0.0", "ssri": "^13.0.0", "supports-color": "^10.2.2", - "tar": "^7.5.2", + "tar": "^7.5.7", "text-table": "~0.2.0", "tiny-relative-date": "^2.0.2", "treeverse": "^3.0.0", - "validate-npm-package-name": "^7.0.0", + "validate-npm-package-name": "^7.0.2", "which": "^6.0.0" }, "bin": { @@ -9822,7 +10040,7 @@ } }, "node_modules/npm/node_modules/@isaacs/brace-expansion": { - "version": "5.0.0", + "version": "5.0.1", "inBundle": true, "license": "MIT", "dependencies": { @@ -9864,7 +10082,7 @@ } }, "node_modules/npm/node_modules/@npmcli/arborist": { - "version": "9.1.8", + "version": "9.2.0", "inBundle": true, "license": "ISC", "dependencies": { @@ -9881,7 +10099,7 @@ "@npmcli/run-script": "^10.0.0", "bin-links": "^6.0.0", "cacache": "^20.0.1", - "common-ancestor-path": "^1.0.1", + "common-ancestor-path": "^2.0.0", "hosted-git-info": "^9.0.0", "json-stringify-nice": "^1.1.4", "lru-cache": "^11.2.1", @@ -9910,7 +10128,7 @@ } }, "node_modules/npm/node_modules/@npmcli/config": { - "version": "10.4.4", + "version": "10.6.0", "inBundle": true, "license": "ISC", "dependencies": { @@ -10091,7 +10309,7 @@ } }, "node_modules/npm/node_modules/@sigstore/core": { - "version": "3.0.0", + "version": "3.1.0", "inBundle": true, "license": "Apache-2.0", "engines": { @@ -10107,48 +10325,40 @@ } }, "node_modules/npm/node_modules/@sigstore/sign": { - "version": "4.0.1", + "version": "4.1.0", "inBundle": true, "license": "Apache-2.0", "dependencies": { "@sigstore/bundle": "^4.0.0", - "@sigstore/core": "^3.0.0", + "@sigstore/core": "^3.1.0", "@sigstore/protobuf-specs": "^0.5.0", - "make-fetch-happen": "^15.0.2", - "proc-log": "^5.0.0", + "make-fetch-happen": "^15.0.3", + "proc-log": "^6.1.0", "promise-retry": "^2.0.1" }, "engines": { "node": "^20.17.0 || >=22.9.0" } }, - "node_modules/npm/node_modules/@sigstore/sign/node_modules/proc-log": { - "version": "5.0.0", - "inBundle": true, - "license": "ISC", - "engines": { - "node": "^18.17.0 || >=20.5.0" - } - }, "node_modules/npm/node_modules/@sigstore/tuf": { - "version": "4.0.0", + "version": "4.0.1", "inBundle": true, "license": "Apache-2.0", "dependencies": { "@sigstore/protobuf-specs": "^0.5.0", - "tuf-js": "^4.0.0" + "tuf-js": "^4.1.0" }, "engines": { "node": "^20.17.0 || >=22.9.0" } }, "node_modules/npm/node_modules/@sigstore/verify": { - "version": "3.0.0", + "version": "3.1.0", "inBundle": true, "license": "Apache-2.0", "dependencies": { "@sigstore/bundle": "^4.0.0", - "@sigstore/core": "^3.0.0", + "@sigstore/core": "^3.1.0", "@sigstore/protobuf-specs": "^0.5.0" }, "engines": { @@ -10164,31 +10374,17 @@ } }, "node_modules/npm/node_modules/@tufjs/models": { - "version": "4.0.0", + "version": "4.1.0", "inBundle": true, "license": "MIT", "dependencies": { "@tufjs/canonical-json": "2.0.0", - "minimatch": "^9.0.5" + "minimatch": "^10.1.1" }, "engines": { "node": "^20.17.0 || >=22.9.0" } }, - "node_modules/npm/node_modules/@tufjs/models/node_modules/minimatch": { - "version": "9.0.5", - "inBundle": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^2.0.1" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/npm/node_modules/abbrev": { "version": "4.0.0", "inBundle": true, @@ -10223,11 +10419,6 @@ "inBundle": true, "license": "MIT" }, - "node_modules/npm/node_modules/balanced-match": { - "version": "1.0.2", - "inBundle": true, - "license": "MIT" - }, "node_modules/npm/node_modules/bin-links": { "version": "6.0.0", "inBundle": true, @@ -10254,14 +10445,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/npm/node_modules/brace-expansion": { - "version": "2.0.2", - "inBundle": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0" - } - }, "node_modules/npm/node_modules/cacache": { "version": "20.0.3", "inBundle": true, @@ -10303,7 +10486,7 @@ } }, "node_modules/npm/node_modules/ci-info": { - "version": "4.3.1", + "version": "4.4.0", "funding": [ { "type": "github", @@ -10348,9 +10531,12 @@ } }, "node_modules/npm/node_modules/common-ancestor-path": { - "version": "1.0.1", + "version": "2.0.0", "inBundle": true, - "license": "ISC" + "license": "BlueOak-1.0.0", + "engines": { + "node": ">= 18" + } }, "node_modules/npm/node_modules/cssesc": { "version": "3.0.0", @@ -10380,7 +10566,7 @@ } }, "node_modules/npm/node_modules/diff": { - "version": "8.0.2", + "version": "8.0.3", "inBundle": true, "license": "BSD-3-Clause", "engines": { @@ -10439,11 +10625,11 @@ } }, "node_modules/npm/node_modules/glob": { - "version": "13.0.0", + "version": "13.0.1", "inBundle": true, "license": "BlueOak-1.0.0", "dependencies": { - "minimatch": "^10.1.1", + "minimatch": "^10.1.2", "minipass": "^7.1.2", "path-scurry": "^2.0.0" }, @@ -10556,7 +10742,7 @@ } }, "node_modules/npm/node_modules/ip-address": { - "version": "10.0.1", + "version": "10.1.0", "inBundle": true, "license": "MIT", "engines": { @@ -10575,11 +10761,11 @@ } }, "node_modules/npm/node_modules/is-cidr": { - "version": "6.0.1", + "version": "6.0.2", "inBundle": true, "license": "BSD-2-Clause", "dependencies": { - "cidr-regex": "5.0.1" + "cidr-regex": "^5.0.1" }, "engines": { "node": ">=20" @@ -10648,11 +10834,11 @@ } }, "node_modules/npm/node_modules/libnpmdiff": { - "version": "8.0.11", + "version": "8.1.0", "inBundle": true, "license": "ISC", "dependencies": { - "@npmcli/arborist": "^9.1.8", + "@npmcli/arborist": "^9.2.0", "@npmcli/installed-package-contents": "^4.0.0", "binary-extensions": "^3.0.0", "diff": "^8.0.2", @@ -10666,11 +10852,11 @@ } }, "node_modules/npm/node_modules/libnpmexec": { - "version": "10.1.10", + "version": "10.2.0", "inBundle": true, "license": "ISC", "dependencies": { - "@npmcli/arborist": "^9.1.8", + "@npmcli/arborist": "^9.2.0", "@npmcli/package-json": "^7.0.0", "@npmcli/run-script": "^10.0.0", "ci-info": "^4.0.0", @@ -10688,11 +10874,11 @@ } }, "node_modules/npm/node_modules/libnpmfund": { - "version": "7.0.11", + "version": "7.0.14", "inBundle": true, "license": "ISC", "dependencies": { - "@npmcli/arborist": "^9.1.8" + "@npmcli/arborist": "^9.2.0" }, "engines": { "node": "^20.17.0 || >=22.9.0" @@ -10711,11 +10897,11 @@ } }, "node_modules/npm/node_modules/libnpmpack": { - "version": "9.0.11", + "version": "9.1.0", "inBundle": true, "license": "ISC", "dependencies": { - "@npmcli/arborist": "^9.1.8", + "@npmcli/arborist": "^9.2.0", "@npmcli/run-script": "^10.0.0", "npm-package-arg": "^13.0.0", "pacote": "^21.0.2" @@ -10781,9 +10967,9 @@ } }, "node_modules/npm/node_modules/lru-cache": { - "version": "11.2.2", + "version": "11.2.5", "inBundle": true, - "license": "ISC", + "license": "BlueOak-1.0.0", "engines": { "node": "20 || >=22" } @@ -10810,11 +10996,11 @@ } }, "node_modules/npm/node_modules/minimatch": { - "version": "10.1.1", + "version": "10.1.2", "inBundle": true, "license": "BlueOak-1.0.0", "dependencies": { - "@isaacs/brace-expansion": "^5.0.0" + "@isaacs/brace-expansion": "^5.0.1" }, "engines": { "node": "20 || >=22" @@ -10843,12 +11029,12 @@ } }, "node_modules/npm/node_modules/minipass-fetch": { - "version": "5.0.0", + "version": "5.0.1", "inBundle": true, "license": "MIT", "dependencies": { "minipass": "^7.0.3", - "minipass-sized": "^1.0.3", + "minipass-sized": "^2.0.0", "minizlib": "^3.0.1" }, "engines": { @@ -10903,22 +11089,11 @@ } }, "node_modules/npm/node_modules/minipass-sized": { - "version": "1.0.3", + "version": "2.0.0", "inBundle": true, "license": "ISC", "dependencies": { - "minipass": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/npm/node_modules/minipass-sized/node_modules/minipass": { - "version": "3.3.6", - "inBundle": true, - "license": "ISC", - "dependencies": { - "yallist": "^4.0.0" + "minipass": "^7.1.2" }, "engines": { "node": ">=8" @@ -10957,7 +11132,7 @@ } }, "node_modules/npm/node_modules/node-gyp": { - "version": "12.1.0", + "version": "12.2.0", "inBundle": true, "license": "MIT", "dependencies": { @@ -10968,7 +11143,7 @@ "nopt": "^9.0.0", "proc-log": "^6.0.0", "semver": "^7.3.5", - "tar": "^7.5.2", + "tar": "^7.5.4", "tinyglobby": "^0.2.12", "which": "^6.0.0" }, @@ -11121,7 +11296,7 @@ } }, "node_modules/npm/node_modules/pacote": { - "version": "21.0.4", + "version": "21.1.0", "inBundle": true, "license": "ISC", "dependencies": { @@ -11164,7 +11339,7 @@ } }, "node_modules/npm/node_modules/path-scurry": { - "version": "2.0.0", + "version": "2.0.1", "inBundle": true, "license": "BlueOak-1.0.0", "dependencies": { @@ -11179,7 +11354,7 @@ } }, "node_modules/npm/node_modules/postcss-selector-parser": { - "version": "7.1.0", + "version": "7.1.1", "inBundle": true, "license": "MIT", "dependencies": { @@ -11308,16 +11483,16 @@ } }, "node_modules/npm/node_modules/sigstore": { - "version": "4.0.0", + "version": "4.1.0", "inBundle": true, "license": "Apache-2.0", "dependencies": { "@sigstore/bundle": "^4.0.0", - "@sigstore/core": "^3.0.0", + "@sigstore/core": "^3.1.0", "@sigstore/protobuf-specs": "^0.5.0", - "@sigstore/sign": "^4.0.0", - "@sigstore/tuf": "^4.0.0", - "@sigstore/verify": "^3.0.0" + "@sigstore/sign": "^4.1.0", + "@sigstore/tuf": "^4.0.1", + "@sigstore/verify": "^3.1.0" }, "engines": { "node": "^20.17.0 || >=22.9.0" @@ -11442,7 +11617,7 @@ } }, "node_modules/npm/node_modules/tar": { - "version": "7.5.2", + "version": "7.5.7", "inBundle": true, "license": "BlueOak-1.0.0", "dependencies": { @@ -11525,13 +11700,13 @@ } }, "node_modules/npm/node_modules/tuf-js": { - "version": "4.0.0", + "version": "4.1.0", "inBundle": true, "license": "MIT", "dependencies": { - "@tufjs/models": "4.0.0", - "debug": "^4.4.1", - "make-fetch-happen": "^15.0.0" + "@tufjs/models": "4.1.0", + "debug": "^4.4.3", + "make-fetch-happen": "^15.0.1" }, "engines": { "node": "^20.17.0 || >=22.9.0" @@ -11583,7 +11758,7 @@ } }, "node_modules/npm/node_modules/validate-npm-package-name": { - "version": "7.0.0", + "version": "7.0.2", "inBundle": true, "license": "ISC", "engines": { @@ -11662,7 +11837,7 @@ }, "node_modules/object-inspect": { "version": "1.13.4", - "devOptional": true, + "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -11758,6 +11933,8 @@ }, "node_modules/on-headers": { "version": "1.1.0", + "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.1.0.tgz", + "integrity": "sha512-737ZY3yNnXy37FHkQxPzt4UZ2UWPWiCZWLvFZ4fu5cueciegX0zGPnrlY6bwRg4FdQOe9YU8MkmJwGhoMybl8A==", "devOptional": true, "license": "MIT", "engines": { @@ -11787,6 +11964,8 @@ }, "node_modules/open": { "version": "6.4.0", + "resolved": "https://registry.npmjs.org/open/-/open-6.4.0.tgz", + "integrity": "sha512-IFenVPgF70fSm1keSd2iDBIDIBZkroLeuffXq+wKTzTJlBpesFWojV9lb8mzOfaAzM1sr7HQHuO0vtV0zYekGg==", "devOptional": true, "license": "MIT", "dependencies": { @@ -11821,6 +12000,8 @@ }, "node_modules/ora": { "version": "5.4.1", + "resolved": "https://registry.npmjs.org/ora/-/ora-5.4.1.tgz", + "integrity": "sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ==", "devOptional": true, "license": "MIT", "dependencies": { @@ -12193,20 +12374,6 @@ ], "license": "MIT" }, - "node_modules/qs": { - "version": "6.13.0", - "devOptional": true, - "license": "BSD-3-Clause", - "dependencies": { - "side-channel": "^1.0.6" - }, - "engines": { - "node": ">=0.6" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/query-string": { "version": "7.1.3", "license": "MIT", @@ -12256,20 +12423,6 @@ "node": ">= 0.6" } }, - "node_modules/raw-body": { - "version": "2.5.2", - "devOptional": true, - "license": "MIT", - "dependencies": { - "bytes": "3.1.2", - "http-errors": "2.0.0", - "iconv-lite": "0.4.24", - "unpipe": "1.0.0" - }, - "engines": { - "node": ">= 0.8" - } - }, "node_modules/react": { "version": "19.1.0", "license": "MIT", @@ -12857,6 +13010,8 @@ }, "node_modules/readable-stream": { "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", "devOptional": true, "license": "MIT", "dependencies": { @@ -12997,6 +13152,8 @@ }, "node_modules/require-main-filename": { "version": "2.0.0", + "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz", + "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==", "devOptional": true, "license": "ISC" }, @@ -13060,6 +13217,8 @@ }, "node_modules/restore-cursor": { "version": "3.1.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-3.1.0.tgz", + "integrity": "sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==", "devOptional": true, "license": "MIT", "dependencies": { @@ -13181,11 +13340,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/safer-buffer": { - "version": "2.1.2", - "devOptional": true, - "license": "MIT" - }, "node_modules/scheduler": { "version": "0.26.0", "license": "MIT" @@ -13276,6 +13430,8 @@ }, "node_modules/set-blocking": { "version": "2.0.0", + "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", + "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==", "devOptional": true, "license": "ISC" }, @@ -13357,7 +13513,7 @@ }, "node_modules/side-channel": { "version": "1.1.0", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "es-errors": "^1.3.0", @@ -13375,7 +13531,7 @@ }, "node_modules/side-channel-list": { "version": "1.0.0", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "es-errors": "^1.3.0", @@ -13390,7 +13546,7 @@ }, "node_modules/side-channel-map": { "version": "1.0.1", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "call-bound": "^1.0.2", @@ -13407,7 +13563,7 @@ }, "node_modules/side-channel-weakmap": { "version": "1.0.2", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "call-bound": "^1.0.2", @@ -13452,6 +13608,8 @@ }, "node_modules/slice-ansi": { "version": "2.1.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-2.1.0.tgz", + "integrity": "sha512-Qu+VC3EwYLldKa1fCxuuvULvSJOKEgk9pi8dZeCVK7TqBfUNTH4sFkk4joj8afVSfAYgJoSOetjx9QWOJ5mYoQ==", "devOptional": true, "license": "MIT", "dependencies": { @@ -13465,6 +13623,8 @@ }, "node_modules/slice-ansi/node_modules/ansi-styles": { "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", "devOptional": true, "license": "MIT", "dependencies": { @@ -13476,6 +13636,8 @@ }, "node_modules/slice-ansi/node_modules/color-convert": { "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", "devOptional": true, "license": "MIT", "dependencies": { @@ -13484,6 +13646,8 @@ }, "node_modules/slice-ansi/node_modules/color-name": { "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", "devOptional": true, "license": "MIT" }, @@ -13580,6 +13744,8 @@ }, "node_modules/string_decoder": { "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", "devOptional": true, "license": "MIT", "dependencies": { @@ -13749,6 +13915,8 @@ }, "node_modules/strnum": { "version": "1.1.2", + "resolved": "https://registry.npmjs.org/strnum/-/strnum-1.1.2.tgz", + "integrity": "sha512-vrN+B7DBIoTTZjnPNewwhx6cBA/H+IS7rfW68n7XxC1y7uoiGQBxaKzqucGUgavX15dJgiGztLJ8vxuEzwqBdA==", "devOptional": true, "funding": [ { @@ -13758,6 +13926,14 @@ ], "license": "MIT" }, + "node_modules/sudo-prompt": { + "version": "9.2.1", + "resolved": "https://registry.npmjs.org/sudo-prompt/-/sudo-prompt-9.2.1.tgz", + "integrity": "sha512-Mu7R0g4ig9TUuGSxJavny5Rv0egCEtpZRNMrZaYS1vxkiIxGiGUwoezU3LazIQ+KE04hTrTfNPgxU5gzi7F5Pw==", + "deprecated": "Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.", + "devOptional": true, + "license": "MIT" + }, "node_modules/supports-color": { "version": "7.2.0", "license": "MIT", @@ -13930,18 +14106,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/type-is": { - "version": "1.6.18", - "devOptional": true, - "license": "MIT", - "dependencies": { - "media-typer": "0.3.0", - "mime-types": "~2.1.24" - }, - "engines": { - "node": ">= 0.6" - } - }, "node_modules/typed-array-buffer": { "version": "1.0.3", "dev": true, @@ -14014,7 +14178,7 @@ }, "node_modules/typescript": { "version": "5.9.2", - "devOptional": true, + "dev": true, "license": "Apache-2.0", "bin": { "tsc": "bin/tsc", @@ -14154,6 +14318,8 @@ }, "node_modules/util-deprecate": { "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", "devOptional": true, "license": "MIT" }, @@ -14179,6 +14345,8 @@ }, "node_modules/vary": { "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", "devOptional": true, "license": "MIT", "engines": { @@ -14202,6 +14370,8 @@ }, "node_modules/wcwidth": { "version": "1.0.1", + "resolved": "https://registry.npmjs.org/wcwidth/-/wcwidth-1.0.1.tgz", + "integrity": "sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg==", "devOptional": true, "license": "MIT", "dependencies": { @@ -14318,6 +14488,8 @@ }, "node_modules/which-module": { "version": "2.0.1", + "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.1.tgz", + "integrity": "sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ==", "devOptional": true, "license": "ISC" }, diff --git a/package.json b/package.json index e69be6f..78755c5 100644 --- a/package.json +++ b/package.json @@ -28,9 +28,11 @@ "@react-navigation/native-stack": "^7.3.26", "@react-navigation/stack": "^7.4.8", "@reduxjs/toolkit": "^2.9.0", + "@tanstack/react-query": "^5.90.21", "@types/react-native-material-ripple": "^0.9.6", "axios": "^1.12.2", "install": "^0.13.0", + "lucide-react-native": "^0.563.0", "npm": "^11.6.0", "react": "19.1.0", "react-native": "0.81.1", @@ -61,9 +63,9 @@ "@babel/core": "^7.25.2", "@babel/preset-env": "^7.25.3", "@babel/runtime": "^7.25.0", - "@react-native-community/cli": "20.0.0", - "@react-native-community/cli-platform-android": "20.0.0", - "@react-native-community/cli-platform-ios": "20.0.0", + "@react-native-community/cli": "^14.0.1", + "@react-native-community/cli-platform-android": "^15.1.0", + "@react-native-community/cli-platform-ios": "^14.0.1", "@react-native/babel-preset": "0.81.1", "@react-native/eslint-config": "0.81.1", "@react-native/metro-config": "0.81.1", diff --git a/src/api/client.ts b/src/api/client.ts new file mode 100644 index 0000000..902d806 --- /dev/null +++ b/src/api/client.ts @@ -0,0 +1,129 @@ +import axios, { AxiosError, InternalAxiosRequestConfig } from 'axios'; +import * as Keychain from 'react-native-keychain'; +import { BASE_URL } from '../utils/ApiUrls'; + +const apiClient = axios.create({ + baseURL: BASE_URL, + headers: { + Accept: 'application/json', + }, +}); + +const buildRequestUrl = (config: InternalAxiosRequestConfig) => { + const requestUrl = config.url ?? ''; + if (/^https?:\/\//i.test(requestUrl)) return requestUrl; + + const base = config.baseURL ?? ''; + if (!base) return requestUrl; + + return `${base.replace(/\/+$/, '')}/${requestUrl.replace(/^\/+/, '')}`; +}; + +const getPayloadByMethod = (config: InternalAxiosRequestConfig) => { + const method = (config.method || 'GET').toUpperCase(); + if (method === 'GET' || method === 'DELETE') { + return config.params ?? null; + } + return config.data ?? null; +}; + +const isFormDataPayload = (payload: unknown): payload is FormData => { + return typeof FormData !== 'undefined' && payload instanceof FormData; +}; + +const getHeaderValue = (headers: InternalAxiosRequestConfig['headers'], name: string) => { + const normalizedName = name.toLowerCase(); + const axiosHeaders = headers as unknown as { + get?: (key: string) => string | undefined; + [key: string]: unknown; + }; + + if (axiosHeaders?.get) { + return axiosHeaders.get(name) || axiosHeaders.get(normalizedName); + } + + const plainHeaders = (headers ?? {}) as Record; + return plainHeaders[name] || plainHeaders[normalizedName]; +}; + +const setHeaderValue = (config: InternalAxiosRequestConfig, name: string, value: string) => { + const axiosHeaders = config.headers as unknown as { + set?: (key: string, value: string) => void; + }; + + if (axiosHeaders?.set) { + axiosHeaders.set(name, value); + return; + } + + config.headers = { + ...(config.headers as Record), + [name]: value, + }; +}; + +const applyRequestContentType = (config: InternalAxiosRequestConfig) => { + const method = (config.method || 'GET').toUpperCase(); + const hasBody = !['GET', 'DELETE', 'HEAD'].includes(method); + const currentContentType = getHeaderValue(config.headers, 'Content-Type'); + + if (isFormDataPayload(config.data)) { + setHeaderValue(config, 'Content-Type', 'multipart/form-data'); + return; + } + + if (hasBody && !currentContentType) { + setHeaderValue(config, 'Content-Type', 'application/json'); + } +}; + +apiClient.interceptors.request.use( + async (config) => { + try { + const credentials = await Keychain.getGenericPassword(); + if (credentials) { + config.headers.Authorization = `Bearer ${credentials.password}`; + } + } catch (error) { + console.error('Error fetching credentials', error); + } + + applyRequestContentType(config); + + const method = (config.method || 'GET').toUpperCase(); + const url = buildRequestUrl(config); + console.log(`[HOOK_API][${method}] URL:`, url); + console.log(`[HOOK_API][${method}] PAYLOAD:`, getPayloadByMethod(config)); + + return config; + }, + (error) => Promise.reject(error) +); + +apiClient.interceptors.response.use( + (response) => { + const method = (response.config.method || 'GET').toUpperCase(); + const url = buildRequestUrl(response.config); + + console.log(`[HOOK_API][${method}] SUCCESS URL:`, url); + console.log(`[HOOK_API][${method}] SUCCESS META:`, { + statusCode: response.status ?? null, + status: response.statusText ?? null, + message: (response.data as { message?: string })?.message ?? null, + }); + console.log(`[HOOK_API][${method}] SUCCESS RESPONSE:`, response.data); + + return response; + }, + (error: AxiosError) => { + const method = (error.config?.method || 'GET').toUpperCase(); + const url = error.config ? buildRequestUrl(error.config as InternalAxiosRequestConfig) : ''; + + console.error(`[HOOK_API][${method}] ERROR URL:`, url || null); + console.error(`[HOOK_API][${method}] ERROR RESPONSE:`, error.response?.data ?? error.message); + + return Promise.reject(error); + } +); + +export default apiClient; diff --git a/src/components/AppView.tsx b/src/components/AppView.tsx index 7f95793..393f12e 100644 --- a/src/components/AppView.tsx +++ b/src/components/AppView.tsx @@ -1,40 +1,97 @@ -import React from "react"; -import { StatusBar, StyleSheet, View, ViewStyle } from "react-native"; -import { SafeAreaView } from "react-native-safe-area-context"; +import React from 'react'; +import { + Keyboard, + KeyboardAvoidingView, + Platform, + StatusBar, + StyleSheet, + TouchableWithoutFeedback, + View, + ViewStyle, +} from 'react-native'; +import { SafeAreaView } from 'react-native-safe-area-context'; +import { useTheme } from '../hooks/ui/useTheme'; type AppViewProps = { children: React.ReactNode; backgroundColor?: string; - barStyle?: "default" | "light-content" | "dark-content"; + barStyle?: 'default' | 'light-content' | 'dark-content'; paddingHorizontal?: number; paddingTop?: number; style?: ViewStyle; + keyboardSafe?: boolean; + keyboardVerticalOffset?: number; + dismissKeyboardOnTap?: boolean; }; export default function AppView({ children, - backgroundColor = "#f5f5f5", - barStyle = "dark-content", - paddingHorizontal = 12, // default - paddingTop = 8, // default + backgroundColor, + barStyle, + paddingHorizontal, + paddingTop, style, + keyboardSafe = true, + keyboardVerticalOffset = 0, + dismissKeyboardOnTap = true, }: AppViewProps) { + const theme = useTheme(); + + const finalBackgroundColor = backgroundColor || theme.colors.background; + const finalBarStyle = barStyle || (theme.mode === 'dark' ? 'light-content' : 'dark-content'); + const finalPaddingHorizontal = paddingHorizontal !== undefined ? paddingHorizontal : theme.spacing.sm; + const finalPaddingTop = paddingTop !== undefined ? paddingTop : 0; + return ( - + - - {children} - + {keyboardSafe ? ( + + {dismissKeyboardOnTap ? ( + + + + {children} + + + + ) : ( + + {children} + + )} + + ) : ( + + {children} + + )} ); } @@ -43,6 +100,9 @@ const styles = StyleSheet.create({ safeArea: { flex: 1, }, + keyboardContainer: { + flex: 1, + }, container: { flex: 1, }, diff --git a/src/components/CostomAlart.tsx b/src/components/CostomAlart.tsx index 4dbc7c2..ea6fd5d 100644 --- a/src/components/CostomAlart.tsx +++ b/src/components/CostomAlart.tsx @@ -8,7 +8,7 @@ import { Platform, } from "react-native"; import { BlurView } from "@react-native-community/blur"; -import { Ionicons } from "@react-native-vector-icons/ionicons"; +import LucideIcon from './common/LucideIcon'; import colors from "./colors"; import { responsiveFontSize } from "react-native-responsive-dimensions"; import { useTheme } from "../theme/ThemeProvider"; @@ -121,7 +121,7 @@ const CustomAlert: React.FC = ({ - + {title} diff --git a/src/components/JoinedGroupCard.tsx b/src/components/JoinedGroupCard.tsx index b1332bb..2041022 100644 --- a/src/components/JoinedGroupCard.tsx +++ b/src/components/JoinedGroupCard.tsx @@ -1,10 +1,12 @@ import React, { FC } from 'react'; import { View, Text, StyleSheet, TouchableOpacity, ToastAndroid } from 'react-native'; -import colors from './colors'; import { useNavigation } from '@react-navigation/native'; -import { BASE_URL, CONTRIBUTE } from '../utils/ApiUrls'; -import { apiCall } from '../utils/ApiHelper'; import { responsiveFontSize } from 'react-native-responsive-dimensions'; +import { useTheme } from '../hooks/ui/useTheme'; +import { useFetch } from '../hooks/core/useFetch'; +import { useCreate } from '../hooks/core/useCreate'; +import { CONTRIBUTE } from '../utils/ApiUrls'; +import FontsFamily from '../constents/FontsFamily'; interface JoinedGroup { id: string; @@ -13,8 +15,8 @@ interface JoinedGroup { visibility: 'PUBLIC' | 'PRIVATE'; createdByUserId: number; createdByName: string; - startDate: [number, number, number] | null; - endDate: [number, number, number] | null; + startDate: string | null; + endDate: string | null; minParticipants: number; maxParticipants: number; currentParticipants: number; @@ -24,138 +26,141 @@ interface JoinedGroup { } interface JoinedGroupCardProps { - item: JoinedGroup | any; - onViewPress: () => void; - onPayPress: () => void; // function for Pay button - hasPaid?: boolean; // whether user has already paid + item: JoinedGroup; + hasPaid?: boolean; } const formatDate = (dateStr: string | null): string => { if (!dateStr) return 'N/A'; - const [year, month, day] = dateStr.split('-'); - return `${day}-${month}-${year}`; + const months = [ + 'Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', + 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec' + ]; + const monthIndex = parseInt(month, 10) - 1; + const dayNum = parseInt(day, 10); + return `${dayNum} ${months[monthIndex]} ${year}`; }; - - -const getStatusStyle = (status: JoinedGroup['status']) => { - switch (status) { - case 'ACTIVE': - return { - backgroundColor: '#D1FAE5', - textColor: '#065F46', - }; - case 'PENDING': - return { - backgroundColor: '#FEF3C7', - textColor: '#92400E', - }; - case 'COMPLETED': - return { - backgroundColor: '#E5E7EB', - textColor: '#4B5563', - }; - default: - return { - backgroundColor: '#E5E7EB', - textColor: '#4B5563', - }; - } -}; - -export const JoinedGroupCard: FC = ({ item, onViewPress, hasPaid }) => { - const progress = (item.currentParticipants / item.maxParticipants) * 100; - const statusStyle = getStatusStyle(item.status); +export const JoinedGroupCard: FC = ({ item }) => { + const theme = useTheme(); const navigation = useNavigation(); - const [loading, setLoading] = React.useState(false); - const [payable, setPayable] = React.useState(); + const { data: payableRes } = useFetch(`${CONTRIBUTE}${item.id}/calculate-contribution`); + const payable = payableRes?.data; + const { mutate: contribute, isPending: loading } = useCreate( + `payments/contribute/${item?.id}?amount=${payable?.totalPayable}`, + { + onSuccess: () => { + ToastAndroid.show('Payment successful!', ToastAndroid.SHORT); + }, + onError: () => { + ToastAndroid.show('Payment failed. Please try again.', ToastAndroid.SHORT); + }, + } + ); - - const onPayPress = async () => { - // Navigate to payment screen or handle payment logic - // navigation.navigate('PaymentScreen', { groupId: item.id, amount: item.contributionAmount }); - setLoading(true); - try { - const res = await apiCall(`${BASE_URL}payments/contribute/${item?.id}?amount=${payable?.totalPayable}`, 'POST') as any; - console.log('payment res ', res) - if (res.status === 200 || res.status === 201 || res.statusCode === 200) { - ToastAndroid.show('Payment done!', ToastAndroid.SHORT); - setLoading(false); - } - } catch (error) { - ToastAndroid.show("An error occurred during payment.", ToastAndroid.SHORT); - } finally { - setLoading(false); + const getStatusColors = (status: JoinedGroup['status']) => { + switch (status) { + case 'ACTIVE': + return { bg: theme.palette.greyExtraLight, text: theme.palette.black }; + case 'COMPLETED': + return { bg: theme.palette.greyLight, text: theme.palette.greyDark }; + default: + return { bg: theme.palette.greyExtraLight, text: theme.palette.greyDark }; } }; - const handleContribution = async () => { - try { - const res = await apiCall(`${CONTRIBUTE}${item.id}/calculate-contribution`, 'GET') as any; - setPayable(res.data); - } catch (error) { - console.log("Error in handleContribution: ", error); - } - }; - React.useEffect(() => { - handleContribution(); - }, []); + const statusColors = getStatusColors(item.status); + return ( - navigation.navigate("GroupDetails", { item, color: colors.black })}> - {/* Header */} + navigation.navigate('GroupDetails', { item })} + activeOpacity={0.7} + > - {item.groupName} - - {item.status} + + {item.groupName} + + by {item.createdByName} + + + + {item.status} - Created by {item.createdByName} - {/* Financial Info */} - + + {/* Contribution Amount — always visible */} + - Contribution - ₹{item.contributionAmount.toLocaleString('en-IN')} + Contribution + + UGX {item.contributionAmount.toLocaleString()} + - Cycle - {item.cycleType} + Type + {item.cycleType} - {payable?.effectiveLateFee && ( - <> - Total Payable: ₹{payable?.totalPayable.toLocaleString('en-IN')} - Effective Late Fee : ₹{payable?.effectiveLateFee.toLocaleString('en-IN')} - + + {/* Payment Details — shown when NOT paid and there's an amount due */} + {payable && !payable.hasPaid && payable.totalPayable > 0 && ( + + + Total Payable + + UGX {payable.totalPayable.toLocaleString()} + + + {payable.effectiveLateFee > 0 && ( + + Late Fee + + + UGX {payable.effectiveLateFee.toLocaleString()} + + + )} + {payable.alreadyPaid > 0 && ( + + Already Paid + + UGX {payable.alreadyPaid.toLocaleString()} + + + )} + )} - {/* Footer with Dates & Buttons */} + - - {formatDate(item.startDate)} to {formatDate(item.endDate)} + + {formatDate(item.startDate)} — {formatDate(item.endDate)} - {/* Add Pay / Paid button here */} - {/* {hasPaid ? ( - - Paid + + {payable?.hasPaid ? ( + + ✓ PAID ) : ( - - - {loading ? 'Loading...' : ' Pay'} - - )} */} - { - hasPaid && - Paid - - } - { - item.status !== 'COMPLETED' && - {loading ? 'Loading...' : ' Pay'} - - } + item.status !== 'COMPLETED' && ( + { + navigation.navigate('PaymentMode' as any, { + groupId: item.id, + routeFrom: 'GroupDetails' + }) + }} + disabled={loading} + > + + {loading ? '...' : 'Pay'} + + + ) + )} ); @@ -163,144 +168,109 @@ export const JoinedGroupCard: FC = ({ item, onViewPress, h const styles = StyleSheet.create({ card: { - backgroundColor: colors.pastelOrange, borderRadius: 16, - padding: 20, - marginBottom: 16, - shadowColor: '#374151', - shadowOpacity: 0.08, - shadowRadius: 10, - shadowOffset: { width: 0, height: 4 }, - elevation: 5, - marginHorizontal: 12 + padding: 14, + marginBottom: 12, + marginHorizontal: 12, + borderWidth: 1, + shadowColor: '#000', + shadowOffset: { width: 0, height: 2 }, + shadowOpacity: 0.05, + shadowRadius: 8, + elevation: 2, }, header: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'flex-start', - marginBottom: 4, + marginBottom: 10, + }, + titleContainer: { + flex: 1, }, groupName: { fontSize: responsiveFontSize(2), - fontWeight: 'bold', - color: '#1F2937', - flexShrink: 1, + fontFamily: FontsFamily.nunitoExtraBold, + marginBottom: 2, }, createdBy: { fontSize: responsiveFontSize(1.4), - color: '#6B7280', - marginBottom: 16, + fontFamily: FontsFamily.nunitoRegular, }, statusBadge: { - paddingHorizontal: 10, - paddingVertical: 5, - borderRadius: 12, + paddingHorizontal: 8, + paddingVertical: 4, + borderRadius: 6, }, statusText: { fontSize: responsiveFontSize(1.2), - fontWeight: '600', - textTransform: 'uppercase', + fontFamily: FontsFamily.nunitoBold, }, infoRow: { flexDirection: 'row', - justifyContent: 'space-between', - paddingVertical: 16, + paddingVertical: 10, borderTopWidth: 1, borderBottomWidth: 1, - borderColor: colors.pastelOrange, - marginBottom: 16, - backgroundColor: colors.white, - borderRadius: 15, + marginBottom: 10, }, infoItem: { - padding: 5, - paddingHorizontal: 10, - backgroundColor: colors.white, - alignItems: 'flex-start', + flex: 1, }, infoLabel: { - fontSize: responsiveFontSize(1.4), - color: '#6B7280', - marginBottom: 4, + fontSize: responsiveFontSize(1.3), + fontFamily: FontsFamily.nunitoRegular, + marginBottom: 2, }, infoValue: { fontSize: responsiveFontSize(1.6), - fontWeight: '600', - color: '#111827', - textTransform: 'capitalize', + fontFamily: FontsFamily.nunitoBold, }, - progressContainer: { - marginBottom: 16, + payableSection: { + marginBottom: 12, + gap: 6, }, - progressLabels: { + payableRow: { flexDirection: 'row', justifyContent: 'space-between', - marginBottom: 8, + alignItems: 'center', }, - participantText: { - fontSize: responsiveFontSize(1.4), - color: '#6B7280', + payableLabel: { + fontSize: responsiveFontSize(1.35), + fontFamily: FontsFamily.nunitoSemiBold, }, - participantCount: { - fontSize: responsiveFontSize(1.4), - fontWeight: '600', - color: '#111827', - }, - progressBarBackground: { - height: 8, - backgroundColor: '#E5E7EB', - borderRadius: 4, - overflow: 'hidden', - }, - progressBarFill: { - height: '100%', - backgroundColor: colors.pastelOrange, - borderRadius: 4, + payableValue: { + fontSize: responsiveFontSize(1.5), + fontFamily: FontsFamily.nunitoBold, }, footer: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', + marginTop: 4, }, dateText: { - fontSize: responsiveFontSize(1.4), - color: '#4B5563', - fontWeight: '500', + fontSize: responsiveFontSize(1.3), + fontFamily: FontsFamily.nunitoMedium, }, - // Pay button payButton: { - elevation: 5, - backgroundColor: colors.white, - paddingHorizontal: 25, - paddingVertical: 10, - borderRadius: 10, + paddingHorizontal: 16, + paddingVertical: 8, + borderRadius: 8, + minWidth: 80, + alignItems: 'center', }, payButtonText: { - color: colors.black, - fontSize: responsiveFontSize(1.4), - fontWeight: '600', + fontSize: responsiveFontSize(1.5), + fontFamily: FontsFamily.nunitoBold, }, - // Paid button - paidButton: { - backgroundColor: '#D1FAE5', - paddingHorizontal: 20, - paddingVertical: 10, - borderRadius: 10, + paidBadge: { + paddingHorizontal: 12, + paddingVertical: 6, + borderRadius: 6, }, - paidButtonText: { - color: '#065F46', - fontSize: responsiveFontSize(1.4), - fontWeight: '600', - }, - viewButton: { - backgroundColor: colors.pastelOrange, - paddingHorizontal: 20, - paddingVertical: 10, - borderRadius: 10, - }, - viewButtonText: { - color: '#000000ff', - fontSize: responsiveFontSize(1.4), - fontWeight: '600', + paidText: { + fontSize: responsiveFontSize(1.3), + fontFamily: FontsFamily.nunitoBold, + letterSpacing: 0.5, }, }); diff --git a/src/components/MyGroupCard.tsx b/src/components/MyGroupCard.tsx index 8058864..5474389 100644 --- a/src/components/MyGroupCard.tsx +++ b/src/components/MyGroupCard.tsx @@ -1,26 +1,21 @@ -// components/MyGroupCard.tsx -import React from "react"; -import { - View, - Text, - StyleSheet, - TouchableOpacity, -} from "react-native"; -import { Ionicons } from "@react-native-vector-icons/ionicons"; -import colors from "./colors"; -import { responsiveFontSize } from "react-native-responsive-dimensions"; +import React from 'react'; +import { View, Text, StyleSheet, TouchableOpacity } from 'react-native'; +import LucideIcon from './common/LucideIcon'; +import { responsiveFontSize } from 'react-native-responsive-dimensions'; +import { useTheme } from '../hooks/ui/useTheme'; +import FontsFamily from '../constents/FontsFamily'; interface MyGroup { id: string; groupName: string; description: string; - visibility: "PUBLIC" | "PRIVATE"; + visibility: 'PUBLIC' | 'PRIVATE'; createdByName: string; currentParticipants: number; maxParticipants: number; contributionAmount: number; - cycleType: "MONTHLY" | "QUARTERLY" | "YEARLY"; - status: "PENDING" | "ACTIVE" | "COMPLETED"; + cycleType: 'CUSTOM' | 'DAILY' | 'WEEKLY' | 'MONTHLY' | 'QUARTERLY' | 'HALF_YEARLY' | 'YEARLY'; + status: 'PENDING' | 'APPROVED' | 'ACTIVE' | 'DELAY' | 'COMPLETED' | 'REJECTED' | 'CANCELLED'; } interface Props { @@ -29,55 +24,74 @@ interface Props { } export const MyGroupCard: React.FC = ({ item, onManage }) => { - const getStatusColor = () => { + const theme = useTheme(); + + const getStatusColors = () => { switch (item.status) { - case "ACTIVE": - return colors.pastelGreen; - case "PENDING": - return colors.pastelOrange; // amber - case "COMPLETED": - return colors.pastelBlue; + case 'ACTIVE': + return { bg: theme.palette.greyExtraLight, text: theme.palette.black }; + case 'APPROVED': + return { bg: theme.colors.success + '22', text: theme.colors.success }; + case 'DELAY': + return { bg: theme.colors.warning + '22', text: theme.colors.warning }; + case 'PENDING': + return { bg: theme.palette.greyExtraLight, text: theme.palette.greyDark }; + case 'COMPLETED': + return { bg: theme.palette.greyLight, text: theme.palette.greyDark }; + case 'REJECTED': + case 'CANCELLED': + return { bg: theme.colors.error + '22', text: theme.colors.error }; default: - return "#adb5bd"; + return { bg: theme.palette.greyExtraLight, text: theme.palette.greyDark }; } }; + const statusColors = getStatusColors(); + return ( - - {/* Left Content */} + - + {item.groupName} - - {item.status} + + {item.status} - + {item.description} - - {item.currentParticipants}/{item.maxParticipants} - - - ₹{item.contributionAmount} - - - {item.cycleType} - + + + + {item.currentParticipants}/{item.maxParticipants} + + + + + + ₹{item.contributionAmount} + + + + + + {item.cycleType} + + - - Created by {item.createdByName} - {/* Manage Button */} - onManage(item)}> - - Manage + onManage(item)} + > + + Manage ); @@ -85,80 +99,72 @@ export const MyGroupCard: React.FC = ({ item, onManage }) => { const styles = StyleSheet.create({ card: { - backgroundColor: "#fff", borderRadius: 12, - padding: 16, - marginBottom: 12, - flexDirection: "row", - alignItems: "center", - elevation: 2, - shadowColor: "#000", + padding: 14, + marginBottom: 10, + flexDirection: 'row', + alignItems: 'center', + borderWidth: 1, + shadowColor: '#000', shadowOpacity: 0.05, shadowRadius: 4, - marginHorizontal:12 + marginHorizontal: 12, + elevation: 2, }, cardHeader: { - flexDirection: "row", - justifyContent: "space-between", - alignItems: "center", - marginBottom: 6, + flexDirection: 'row', + justifyContent: 'space-between', + alignItems: 'center', + marginBottom: 8, }, cardTitle: { fontSize: responsiveFontSize(1.8), - fontWeight: "bold", - color: "#343a40", + fontFamily: FontsFamily.nunitoExtraBold, flex: 1, marginRight: 10, }, cardSubtitle: { fontSize: responsiveFontSize(1.4), - color: "#6c757d", - marginBottom: 8, + fontFamily: FontsFamily.nunitoRegular, + marginBottom: 10, }, cardMetaContainer: { - flexDirection: "row", - alignItems: "center", - flexWrap: "wrap", - marginBottom: 6, + flexDirection: 'row', + alignItems: 'center', + flexWrap: 'wrap', }, - manageButtonText: { - fontSize: responsiveFontSize(1.2), - color: colors.textDark, + metaItem: { + flexDirection: 'row', + alignItems: 'center', + marginRight: 16, + marginBottom: 4, }, cardMetaText: { fontSize: responsiveFontSize(1.3), - color: "#495057", - fontWeight: "500", - marginRight: 16, - flexDirection: "row", - alignItems: "center", + fontFamily: FontsFamily.nunitoSemiBold, + marginLeft: 4, }, statusBadge: { - paddingHorizontal: 10, + paddingHorizontal: 8, paddingVertical: 4, - borderRadius: 5, - elevation: 1, + borderRadius: 4, }, statusBadgeText: { - color: colors.textDark, fontSize: responsiveFontSize(1), - fontWeight: "bold", - textTransform: "uppercase", + fontFamily: FontsFamily.nunitoBold, + textTransform: 'uppercase', }, manageButton: { - width: 100, - borderRadius:10, - flexDirection:"row", - alignItems: "center", - justifyContent: "space-around", - padding: 6, + width: 84, + borderRadius: 8, + alignItems: 'center', + justifyContent: 'center', + paddingVertical: 10, marginLeft: 10, - elevation:2, - backgroundColor: colors.pastelOrange, }, - createdBy: { + manageButtonText: { fontSize: responsiveFontSize(1.2), - color: "#868e96", + fontFamily: FontsFamily.nunitoBold, marginTop: 4, }, }); diff --git a/src/components/RequestCard.tsx b/src/components/RequestCard.tsx index 9c070a9..d1f3a7a 100644 --- a/src/components/RequestCard.tsx +++ b/src/components/RequestCard.tsx @@ -1,11 +1,10 @@ -// src/components/RequestCard.tsx - import React, { FC } from 'react'; import { View, Text, Image, TouchableOpacity, StyleSheet, Platform } from 'react-native'; -import { Ionicons } from "@react-native-vector-icons/ionicons"; // Assuming Expo for icons -import colors from './colors'; // Your color palette -import { JoinRequest } from '../types'; // Your type definition +import LucideIcon from './common/LucideIcon'; import { responsiveFontSize } from 'react-native-responsive-dimensions'; +import { useTheme } from '../hooks/ui/useTheme'; +import { JoinRequest } from '../types'; +import FontsFamily from '../constents/FontsFamily'; interface RequestCardProps { item: JoinRequest; @@ -14,46 +13,57 @@ interface RequestCardProps { } export const RequestCard: FC = ({ item, onApprove, onDecline }) => { + const theme = useTheme(); + return ( - - {/* User Information Section */} + - + - {item.userName} + + {item.userName} + - - {item.mobile} + + {item.mobile} - {/* Group Information Section */} - - Wants to join group - {item.groupName} + + + Wants to join group + + + {item.groupName} + - {/* Action Buttons Section */} - onDecline(item)} activeOpacity={0.7} > - - Decline + + Decline - onApprove(item)} activeOpacity={0.8} > - - Approve + + Approve @@ -62,110 +72,82 @@ export const RequestCard: FC = ({ item, onApprove, onDecline } const styles = StyleSheet.create({ card: { - marginHorizontal: 16, - marginVertical: 12, - backgroundColor: colors.white, - borderRadius: 24, - padding: 20, + marginHorizontal: 12, + marginVertical: 8, + padding: 16, + borderWidth: 1, ...Platform.select({ ios: { shadowColor: '#000', shadowOffset: { width: 0, height: 4 }, - shadowOpacity: 0.08, - shadowRadius: 12, + shadowOpacity: 0.05, + shadowRadius: 10, }, android: { - elevation: 8, - shadowColor: '#00000020', // Softer shadow color for Android + elevation: 3, }, }), - borderWidth: 1, - borderColor: '#f0f0f0', }, userInfoSection: { flexDirection: 'row', alignItems: 'center', }, avatar: { - width: 60, - height: 60, - borderRadius: 30, - marginRight: 16, - borderWidth: 2, - borderColor: colors.pastelOrange, + width: 52, + height: 52, + borderRadius: 26, + marginRight: 14, + backgroundColor: '#E6E6E6', }, userInfo: { flex: 1, }, userName: { - fontSize: responsiveFontSize(1.9), - fontWeight: 'bold', - color: colors.textDark, + fontSize: responsiveFontSize(1.8), + fontFamily: FontsFamily.nunitoExtraBold, }, userMetaContainer: { flexDirection: 'row', alignItems: 'center', - marginTop: 5, + marginTop: 4, }, userMeta: { fontSize: responsiveFontSize(1.4), - color: colors.textLight, - marginLeft: 8, + fontFamily: FontsFamily.nunitoRegular, + marginLeft: 6, }, groupInfoSection: { - marginTop: 18, - paddingVertical: 16, - paddingHorizontal: 16, - backgroundColor: '#F7F8FA', - borderRadius: 16, + marginTop: 14, + padding: 14, + borderRadius: 12, alignItems: 'center', }, requestText: { - fontSize: responsiveFontSize(1.4), - color: colors.textLight, + fontSize: responsiveFontSize(1.3), + fontFamily: FontsFamily.nunitoRegular, }, groupName: { - fontSize: responsiveFontSize(1.7), - fontWeight: '600', - color: colors.textDark, - marginTop: 6, + fontSize: responsiveFontSize(1.6), + fontFamily: FontsFamily.nunitoBold, + marginTop: 4, textAlign: 'center', }, actionsSection: { flexDirection: 'row', - marginTop: 20, - gap: 12, + marginTop: 16, + gap: 10, }, actionButton: { flex: 1, flexDirection: 'row', alignItems: 'center', justifyContent: 'center', - paddingVertical: 16, - borderRadius: 16, - }, - declineButton: { - backgroundColor: '#F1F3F6', - }, - approveButton: { - backgroundColor: colors.pastelOrange, - ...Platform.select({ - ios: { - shadowColor: colors.pastelOrange, - shadowOffset: { width: 0, height: 4 }, - shadowOpacity: 0.3, - shadowRadius: 8, - }, - android: { - elevation: 6, - shadowColor: colors.pastelOrange, - }, - }), + paddingVertical: 12, + borderRadius: 12, }, actionButtonText: { - fontSize: responsiveFontSize(1.6), - fontWeight: 'bold', + fontSize: responsiveFontSize(1.5), + fontFamily: FontsFamily.nunitoBold, marginLeft: 8, - color: colors.textDark, }, }); diff --git a/src/components/UserRequested.tsx b/src/components/UserRequested.tsx index a6f6999..cf14145 100644 --- a/src/components/UserRequested.tsx +++ b/src/components/UserRequested.tsx @@ -1,42 +1,43 @@ import React, { FC } from "react"; import { View, Text, StyleSheet } from "react-native"; -import { Ionicons } from "@react-native-vector-icons/ionicons"; -import colors from "./colors"; +import LucideIcon from './common/LucideIcon'; import { UserJoinRequest } from "../types"; import { formatDate } from "../utils/HelperFunction"; import { responsiveFontSize } from "react-native-responsive-dimensions"; +import { useTheme } from "../hooks/ui/useTheme"; +import FontsFamily from "../constents/FontsFamily"; interface UserRequestedProps { item: UserJoinRequest; } -const getStatusStyle = (status: UserJoinRequest["status"]) => { +const getStatusStyle = (status: UserJoinRequest["status"], theme: ReturnType) => { switch (status) { case "APPROVED": return { - backgroundColor: "#D1FAE5", - textColor: "#065F46", + backgroundColor: theme.colors.success + "22", + textColor: theme.colors.success, label: "Approved", icon: "checkmark-circle-outline", }; case "PENDING": return { - backgroundColor: "#FEF3C7", - textColor: "#92400E", + backgroundColor: theme.colors.warning + "22", + textColor: theme.colors.warning, label: "Pending", icon: "time-outline", }; case "REJECT": return { - backgroundColor: "#FEE2E2", - textColor: "#B91C1C", + backgroundColor: theme.colors.error + "22", + textColor: theme.colors.error, label: "Rejected", icon: "close-circle-outline", }; default: return { - backgroundColor: "#E5E7EB", - textColor: "#4B5563", + backgroundColor: theme.colors.borderLight, + textColor: theme.colors.textSecondary, label: "Unknown", icon: "help-outline", }; @@ -44,41 +45,40 @@ const getStatusStyle = (status: UserJoinRequest["status"]) => { }; export const UserRequested: FC = ({ item }) => { - const { backgroundColor, textColor, label, icon } = getStatusStyle(item.status); - - // Optionally, initials avatar (remove/comment if you want): - const initials = item.createdBy?.trim().split(" ").map((w) => w[0])?.join("").toUpperCase(); + const theme = useTheme(); + const { backgroundColor, textColor, label, icon } = getStatusStyle(item.status, theme); + const initials = item.createdBy?.trim().split(" ").map((w) => w[0])?.join("").toUpperCase() || "?"; return ( - - + + - - {initials} + + {initials} - {item.groupName} - - {" "} + {item.groupName} + + {" "} {formatDate(item.requestDate)} - + {label} - - + + {" "}Group Created By:{" "} - {item.createdBy} + {item.createdBy} {item.reason ? ( - - {item.reason} + + {item.reason} ) : null} @@ -88,79 +88,73 @@ export const UserRequested: FC = ({ item }) => { const styles = StyleSheet.create({ card: { - backgroundColor: colors.white, - borderRadius: 16, - marginVertical: 14, // More space - marginHorizontal: 10, - elevation: 4, + borderRadius: 14, + marginVertical: 8, + marginHorizontal: 12, + elevation: 2, + borderWidth: 1, shadowColor: "#000", - shadowOpacity: 0.12, - shadowRadius: 8, + shadowOpacity: 0.08, + shadowRadius: 6, shadowOffset: { width: 0, height: 2 }, - padding: 16, + padding: 14, }, topRow: { flexDirection: "row", alignItems: "flex-start", borderBottomWidth: 1, - borderBottomColor: "#e5e7eb", - paddingBottom: 10, - marginBottom: 6 + paddingBottom: 8, + marginBottom: 6, }, avatarContainer: { - marginRight: 10, + marginRight: 8, marginTop: 2, }, avatarCircle: { width: 36, height: 36, borderRadius: 18, - backgroundColor: colors.pastelBlue, justifyContent: "center", alignItems: "center" }, avatarText: { - fontWeight: "700", + fontFamily: FontsFamily.nunitoBold, fontSize: responsiveFontSize(1.6), - color: colors.white, }, userInfoSection: { flex: 1, - paddingRight: 8, + paddingRight: 6, }, userName: { - fontWeight: "bold", - color: colors.textDark, + fontFamily: FontsFamily.nunitoBold, fontSize: responsiveFontSize(1.5), }, userMeta: { fontSize: responsiveFontSize(1.2), - color: colors.textLight, + fontFamily: FontsFamily.nunitoRegular, marginTop: 3, marginBottom: -3, }, groupInfoSection: { - paddingTop: 10, - paddingBottom: 6, + paddingTop: 8, + paddingBottom: 2, }, requestText: { - color: colors.textDark, fontSize: responsiveFontSize(1.5), marginBottom: 2, - lineHeight: 22, + lineHeight: 20, + fontFamily: FontsFamily.nunitoRegular, }, groupName: { - fontWeight: "600", - color: colors.Settings, + fontFamily: FontsFamily.nunitoSemiBold, fontSize: responsiveFontSize(1.8), marginBottom: 2, }, reasonText: { fontSize: responsiveFontSize(1.2), - color: colors.textLight, marginTop: 6, paddingLeft: 1, - lineHeight: 18, + lineHeight: 16, fontStyle: "italic", }, statusBadge: { @@ -170,15 +164,15 @@ const styles = StyleSheet.create({ paddingHorizontal: 10, paddingVertical: 5, marginLeft: 4, - minWidth: 90, + minWidth: 84, justifyContent: "center", alignSelf: "flex-start", marginTop: 2 }, statusText: { fontSize: responsiveFontSize(1.4), - fontWeight: "700", + fontFamily: FontsFamily.nunitoBold, marginLeft: 6, - letterSpacing: 0.5 + letterSpacing: 0.3 }, }); diff --git a/src/components/common/AnimatedCard.tsx b/src/components/common/AnimatedCard.tsx index 5243710..b0acd04 100644 --- a/src/components/common/AnimatedCard.tsx +++ b/src/components/common/AnimatedCard.tsx @@ -1,44 +1,62 @@ -import React, { use, useRef } from "react"; -import { View, Text, StyleSheet, Animated, TouchableWithoutFeedback } from "react-native"; -import LinearGradient from "react-native-linear-gradient"; -import { Ionicons } from "@react-native-vector-icons/ionicons"; -import colors from "../colors"; -import { responsiveFontSize } from "react-native-responsive-dimensions"; -import { useTheme } from "../../theme/ThemeProvider"; -import FontsFamily from "../../constents/FontsFamily"; +import React, { useRef } from 'react'; +import { View, Text, StyleSheet, Animated, TouchableWithoutFeedback } from 'react-native'; +import LucideIcon from './LucideIcon'; +import { responsiveFontSize } from 'react-native-responsive-dimensions'; +import { useTheme } from '../../hooks/ui/useTheme'; +import FontsFamily from '../../constents/FontsFamily'; type CardProps = { title: string; subtitle?: string; iconName?: string; onPress?: () => void; - backOne?: string; - backTwo?: string; }; const AnimatedCard: React.FC = ({ title, subtitle, - iconName = "ellipse-outline", + iconName = 'ellipse-outline', onPress, - backOne = colors.primary, - backTwo = colors.pastelPink, }) => { + const theme = useTheme(); const scale = useRef(new Animated.Value(1)).current; - const{ theme } = useTheme() - const handlePress = () => { - Animated.sequence([ - Animated.spring(scale, { toValue: 0.95, useNativeDriver: true }), - ]).start(() => onPress && onPress()); + + const handlePressIn = () => { + Animated.spring(scale, { toValue: 0.95, useNativeDriver: true }).start(); + }; + + const handlePressOut = () => { + Animated.spring(scale, { toValue: 1, useNativeDriver: true }).start(); }; return ( - - - - - {title} - {subtitle && {subtitle}} + + + + + {title} + {subtitle && ( + {subtitle} + )} @@ -48,20 +66,33 @@ const AnimatedCard: React.FC = ({ const styles = StyleSheet.create({ card: { flex: 1, - height: 160, - margin: 8, - borderRadius: 20, - overflow: "hidden", - elevation: 5, + height: 150, + margin: 6, + borderWidth: 1, + elevation: 2, + shadowColor: '#000', + shadowOffset: { width: 0, height: 2 }, + shadowOpacity: 0.08, + shadowRadius: 4, }, - gradient: { + content: { flex: 1, - justifyContent: "center", - alignItems: "center", + justifyContent: 'center', + alignItems: 'center', + padding: 16, + }, + icon: { marginBottom: 12 }, + title: { + fontSize: responsiveFontSize(2), + fontFamily: FontsFamily.nunitoExtraBold, + textAlign: 'center', + }, + subtitle: { + fontSize: responsiveFontSize(1.4), + fontFamily: FontsFamily.nunitoRegular, + textAlign: 'center', + marginTop: 4, }, - icon: { marginBottom: 10 }, - title: { fontSize: responsiveFontSize(2.2), fontFamily: FontsFamily.nunitoExtraBold, color: "#fff" }, - subtitle: { fontSize: responsiveFontSize(1.5), fontFamily: FontsFamily.nunitoRegular, opacity: 0.9 }, }); export default AnimatedCard; diff --git a/src/components/common/AnimatedText.tsx b/src/components/common/AnimatedText.tsx index a1418ba..cf06f9c 100644 --- a/src/components/common/AnimatedText.tsx +++ b/src/components/common/AnimatedText.tsx @@ -62,4 +62,4 @@ const styles = StyleSheet.create({ text: { textAlign: 'center', }, -}); \ No newline at end of file +}); diff --git a/src/components/common/Custom.tsx b/src/components/common/Custom.tsx index 1fc1d86..cbc82e7 100644 --- a/src/components/common/Custom.tsx +++ b/src/components/common/Custom.tsx @@ -1,46 +1,76 @@ -import React from "react"; -import { - StyleSheet, - View, - Text, - TextInput, - TouchableOpacity, - TextInputProps, -} from "react-native"; -import {Ionicons, IoniconsIconName} from "@react-native-vector-icons/ionicons"; -import colors from "../colors"; -import { responsiveFontSize } from "react-native-responsive-dimensions"; +import React from 'react'; +import { StyleSheet, View, Text, TextInput, TouchableOpacity, TextInputProps } from 'react-native'; +import LucideIcon, { LucideIconName } from './LucideIcon'; +import { responsiveFontSize } from 'react-native-responsive-dimensions'; +import { useTheme } from '../../hooks/ui/useTheme'; // --- Custom Input --- type CustomInputProps = TextInputProps & { label?: string; - icon?: IoniconsIconName; - error?:string + icon?: string; + error?: string; value?: string | number; }; -const CustomInput: React.FC = ({ - label, - value, - icon, - style, - error, - ...rest -}) => { +const withOpacity = (hex: string, alpha: number) => { + const normalized = hex.replace('#', ''); + if (normalized.length !== 6) return hex; + + const r = parseInt(normalized.slice(0, 2), 16); + const g = parseInt(normalized.slice(2, 4), 16); + const b = parseInt(normalized.slice(4, 6), 16); + return `rgba(${r}, ${g}, ${b}, ${alpha})`; +}; + +const CustomInput: React.FC = ({ label, value, icon, style, error, placeholder, ...rest }) => { + const theme = useTheme(); + const computedPlaceholder = placeholder ?? (label ? `Enter ${label.toLowerCase()}` : undefined); + const hasError = Boolean(error && String(error).trim()); + return ( - {label ? {label} : null} + {label ? {label} : null} - {icon && } + {icon && ( + + )} + {hasError ? ( + + + {error} + + ) : null} ); }; @@ -49,32 +79,72 @@ const CustomInput: React.FC = ({ type CustomButtonProps = { title: string; onPress: () => void; - type?: "primary" | "secondary" | "danger"; - icon?: IoniconsIconName; + type?: 'primary' | 'secondary' | 'outline'; + icon?: string; style?: object; buttonBackground?: string; + textColor?: string; + loading?: boolean; + disabled?: boolean; }; const CustomButton: React.FC = ({ title, onPress, - type = "primary", + type = 'primary', icon, style = {}, - buttonBackground, + buttonBackground, + textColor: customTextColor, + loading = false, + disabled = false, }) => { + const theme = useTheme(); - const textColor = type === "secondary" ? colors.textDark : '#000'; + const getBackgroundColor = () => { + if (disabled) return theme.palette.greyLight; + if (buttonBackground) return buttonBackground; + if (type === 'primary') return theme.colors.primary; + if (type === 'secondary') return theme.palette.greyMedium; + return 'transparent'; + }; + + const getTextColor = () => { + if (disabled) return theme.colors.textMuted; + if (customTextColor) return customTextColor; + if (type === 'primary') return theme.colors.onPrimary; + if (type === 'secondary') return theme.colors.textPrimary; + if (type === 'outline') return theme.colors.primary; + return theme.colors.onPrimary; + }; + + const backgroundColor = getBackgroundColor(); + const textColor = getTextColor(); return ( - {icon && } - {title} + {icon && !loading && ( + + )} + + {loading ? 'Processing...' : title} + ); @@ -82,42 +152,72 @@ const CustomButton: React.FC = ({ // --- Styles --- const styles = StyleSheet.create({ - inputContainer: { marginBottom: 12,elevation:5 }, - inputLabel: { fontSize: responsiveFontSize(1.5), fontWeight: "500", color: colors.textDark, marginBottom: 6 }, + inputContainer: { + marginBottom: 14, + }, + inputLabel: { + fontSize: responsiveFontSize(1.6), + fontWeight: '600', + marginBottom: 8, + }, inputWrapper: { - flexDirection: "row", - alignItems: "center", - position: "relative", + flexDirection: 'row', + alignItems: 'center', + position: 'relative', }, inputIcon: { - position: "absolute", - left: 10, + position: 'absolute', + left: 12, zIndex: 1, }, input: { flex: 1, - borderRadius: 15, - padding: 12, - paddingLeft: 12, - backgroundColor: colors.white, - color: colors.textDark, - fontSize: responsiveFontSize(1.5), + borderRadius: 12, + paddingHorizontal: 12, + paddingVertical: 12, + fontSize: responsiveFontSize(1.6), + }, + errorContainer: { + marginTop: 6, + borderWidth: 1, + borderRadius: 10, + paddingHorizontal: 10, + paddingVertical: 6, + flexDirection: 'row', + alignItems: 'center', + gap: 6, + }, + errorText: { + fontSize: responsiveFontSize(1.25), + fontWeight: '600', + flex: 1, }, button: { - elevation:5, - paddingVertical: 12, + paddingVertical: 14, borderRadius: 12, - alignItems: "center", + alignItems: 'center', marginVertical: 6, + // Soft shadow for depth + shadowColor: '#000', + shadowOffset: { width: 0, height: 2 }, + shadowOpacity: 0.08, + shadowRadius: 4, + elevation: 2, + }, + outlineButton: { + shadowOpacity: 0, + shadowRadius: 0, + shadowOffset: { width: 0, height: 0 }, + elevation: 0, }, buttonContent: { - flexDirection: "row", - alignItems: "center", - justifyContent: "center", + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'center', }, buttonText: { - fontSize: responsiveFontSize(1.6), - fontWeight: "600", + fontSize: responsiveFontSize(1.8), + fontWeight: '700', }, }); diff --git a/src/components/common/CustomModal.tsx b/src/components/common/CustomModal.tsx index dba0e72..c9d32e5 100644 --- a/src/components/common/CustomModal.tsx +++ b/src/components/common/CustomModal.tsx @@ -1,4 +1,4 @@ -import React, { useRef, useEffect } from "react"; +import React, { useRef, useEffect } from 'react'; import { View, Text, @@ -7,11 +7,16 @@ import { TouchableOpacity, Animated, Dimensions, -} from "react-native"; -import colors from "../colors"; -import { responsiveFontSize } from "react-native-responsive-dimensions"; + KeyboardAvoidingView, + Platform, + TouchableWithoutFeedback, + Keyboard, +} from 'react-native'; +import { responsiveFontSize } from 'react-native-responsive-dimensions'; +import { useTheme } from '../../hooks/ui/useTheme'; +import FontsFamily from '../../constents/FontsFamily'; -const { height } = Dimensions.get("window"); +const { height } = Dimensions.get('window'); type CustomModalProps = { visible: boolean; @@ -21,9 +26,9 @@ type CustomModalProps = { confirmText?: string; cancelText?: string; onConfirm?: () => void; - confirmButton?:string - confirmTextColor?:string - elevation?:number + confirmButton?: string; + confirmTextColor?: string; + elevation?: number; }; const CustomModal: React.FC = ({ @@ -31,14 +36,15 @@ const CustomModal: React.FC = ({ onClose, title, children, - confirmText = "Confirm", - cancelText = "Cancel", + confirmText = 'Confirm', + cancelText = 'Cancel', onConfirm, confirmTextColor, - elevation, - confirmButton = colors.primary + elevation = 5, + confirmButton, }) => { - const slideAnim = useRef(new Animated.Value(height)).current; // starts off screen + const theme = useTheme(); + const slideAnim = useRef(new Animated.Value(height)).current; const fadeAnim = useRef(new Animated.Value(0)).current; useEffect(() => { @@ -69,45 +75,65 @@ const CustomModal: React.FC = ({ }), ]).start(); } - }, [visible]); + }, [fadeAnim, slideAnim, visible]); return ( - {/* Overlay */} - {/* Modal Box */} - - {/* Header */} - { - title && + + + + {title && ( + {title} + )} + {children} - {title} - } - {/* Body */} - {children} + + + {cancelText} + - {/* Footer Actions */} - - - {cancelText} - - - - {confirmText} - - - + + + {confirmText} + + + + + + + ); @@ -118,46 +144,41 @@ export default CustomModal; const styles = StyleSheet.create({ overlay: { flex: 1, - backgroundColor: "rgba(0,0,0,0.4)", - justifyContent: "flex-end", + backgroundColor: 'rgba(0,0,0,0.5)', + justifyContent: 'flex-end', + }, + keyboardContainer: { + flex: 1, + justifyContent: 'flex-end', }, modalBox: { - backgroundColor: "#fff", - borderTopLeftRadius: 20, - borderTopRightRadius: 20, - padding: 20, + padding: 24, elevation: 10, }, title: { - fontSize: responsiveFontSize(1.8), - fontWeight: "700", - color: "#1f2937", - marginBottom: 12, + fontSize: responsiveFontSize(2), + fontFamily: FontsFamily.nunitoBold, + marginBottom: 16, }, content: { - marginBottom: 20, + marginBottom: 24, }, actions: { - flexDirection: "row", - justifyContent: "flex-end", - marginTop: 10, + flexDirection: 'row', + justifyContent: 'flex-end', + gap: 12, }, button: { - paddingVertical: 10, - paddingHorizontal: 18, - borderRadius: 10, - marginLeft: 10, + paddingVertical: 12, + paddingHorizontal: 24, + borderRadius: 12, }, - cancelButton: { - backgroundColor: "#e5e7eb", - }, - cancelText: { - color: "#374151", - fontWeight: "500", + fontFamily: FontsFamily.nunitoSemiBold, + fontSize: responsiveFontSize(1.6), }, confirmText: { - color: "#fff", - fontWeight: "600", + fontFamily: FontsFamily.nunitoBold, + fontSize: responsiveFontSize(1.6), }, }); diff --git a/src/components/common/Header.tsx b/src/components/common/Header.tsx index a2600ea..91685e1 100644 --- a/src/components/common/Header.tsx +++ b/src/components/common/Header.tsx @@ -1,9 +1,10 @@ import React from "react"; import { View, Text, TouchableOpacity, StyleSheet } from "react-native"; import { useNavigation } from "@react-navigation/native"; -import {Ionicons} from "@react-native-vector-icons/ionicons"; -import colors from "../colors"; +import LucideIcon from './LucideIcon'; import { responsiveFontSize } from "react-native-responsive-dimensions"; +import { useTheme } from "../../hooks/ui/useTheme"; +import FontsFamily from "../../constents/FontsFamily"; type HeaderProps = { title: string; @@ -19,10 +20,14 @@ const Header: React.FC = ({ showBack = true, onBackPress, rightAction, - titleColor = '#000', - arrowColor = '#000' + titleColor, + arrowColor }) => { const navigation = useNavigation(); + const theme = useTheme(); + + const resolvedTitleColor = titleColor || theme.colors.textPrimary; + const resolvedArrowColor = arrowColor || theme.colors.textPrimary; const handleBack = () => { if (onBackPress) { @@ -35,12 +40,15 @@ const Header: React.FC = ({ return ( {showBack && ( - - + + - )} - {title} - {rightAction} + )} + {!showBack && } + + {title} + + {rightAction} ); }; @@ -50,15 +58,21 @@ export default Header; const styles = StyleSheet.create({ header: { flexDirection: "row", - // alignItems: "center", - // justifyContent: "space-between", - paddingVertical: 12, - paddingHorizontal:12 + alignItems: "center", + justifyContent: "space-between", + paddingVertical: 10, + paddingHorizontal: 12, + }, + sideAction: { + width: 36, + height: 36, + alignItems: "center", + justifyContent: "center", }, headerTitle: { - fontSize: responsiveFontSize(1.8), - fontWeight: "600", - // textTransform: "capitalize", + flex: 1, + textAlign: "center", + fontSize: responsiveFontSize(2), + fontFamily: FontsFamily.nunitoBold, }, - }); diff --git a/src/components/common/LucideIcon.tsx b/src/components/common/LucideIcon.tsx new file mode 100644 index 0000000..4591a3e --- /dev/null +++ b/src/components/common/LucideIcon.tsx @@ -0,0 +1,197 @@ +import React from 'react'; +import { + Home, User, Users, UsersRound, Settings, Bell, BellOff, + ArrowLeft, ArrowRight, ChevronRight, ChevronDown, X, XCircle, + Search, Wallet, Banknote, Trophy, Receipt, PlusCircle, + MailOpen, Mail, Pencil, Camera, CloudUpload, LogOut, + AlertCircle, Info, CheckCircle, CheckSquare, Square, + Phone, Trash2, MoreVertical, Power, Repeat, Ticket, + PieChart, BookOpen, Globe, FileText, UserPlus, Sparkles, + Cpu, Circle, Copy, Star, Calendar, Clock, ArrowUpDown, + Link, ShieldCheck, ChevronUp, Eye, EyeOff, Building2, + CreditCard, Hash, Check, HelpCircle, MessageCircle, CheckCircle2, + RefreshCw, ArrowDownCircle, CircleUserRound, + AtSign, MapPin, Barcode, Flag, Send, Tag, Code, + type LucideProps, +} from 'lucide-react-native'; + +// Map of Ionicons names to Lucide components +const iconMap: Record> = { + // Home + 'home': Home, + 'home-outline': Home, + // User / Person + 'person': User, + 'person-outline': User, + 'people': Users, + 'people-outline': Users, + 'people-circle-outline': UsersRound, + 'person-add-outline': UserPlus, + 'person-circle-outline': CircleUserRound, + // Settings + 'settings': Settings, + 'settings-outline': Settings, + // Notifications + 'notifications-outline': Bell, + 'notifications-off-outline': BellOff, + // Navigation + 'arrow-back': ArrowLeft, + 'arrow-forward': ArrowRight, + 'arrow-down-circle-outline': ArrowDownCircle, + 'chevron-forward': ChevronRight, + 'chevron-down': ChevronDown, + 'chevron-up': ChevronUp, + 'swap-vertical': ArrowUpDown, + // Close + 'close': X, + 'close-circle': XCircle, + // Search + 'search': Search, + // Finance + 'wallet-outline': Wallet, + 'cash': Banknote, + 'cash-outline': Banknote, + 'card-outline': CreditCard, + // Trophy + 'trophy': Trophy, + // Receipt + 'receipt-outline': Receipt, + // Add + 'add-circle-outline': PlusCircle, + 'add': PlusCircle, + // Mail + 'mail-open-outline': MailOpen, + 'mail-unread-outline': Mail, + // Edit + 'create-outline': Pencil, + // Camera + 'camera': Camera, + // Upload + 'cloud-upload': CloudUpload, + // Auth + 'log-out-outline': LogOut, + // Alert / Info + 'alert-circle': AlertCircle, + 'alert-circle-outline': AlertCircle, + 'information-circle-outline': Info, + // Check + 'checkmark-circle': CheckCircle, + 'checkbox': CheckSquare, + 'square-outline': Square, + // Phone + 'call-outline': Phone, + // Delete + 'trash-outline': Trash2, + // More + 'ellipsis-vertical': MoreVertical, + // Power + 'power': Power, + // Repeat + 'repeat-outline': Repeat, + // Ticket + 'ticket-outline': Ticket, + // Charts + 'pie-chart-outline': PieChart, + // Book + 'book-outline': BookOpen, + // Globe + 'planet-outline': Globe, + // Documents + 'documents-outline': FileText, + // Sparkles + 'sparkles-outline': Sparkles, + // Hardware + 'hardware-chip-outline': Cpu, + // Shapes + 'ellipse-outline': Circle, + // Copy + 'copy-outline': Copy, + // Star + 'star': Star, + 'star-outline': Star, + // Calendar + 'calendar-outline': Calendar, + // Time + 'time-outline': Clock, + // Link + 'link-outline': Link, + // Shield + 'shield-checkmark-outline': ShieldCheck, + // Eye + 'eye-outline': Eye, + 'eye-off-outline': EyeOff, + // Building + 'business-outline': Building2, + // Hash + 'keypad-outline': Hash, + // Check + 'checkmark': Check, + 'checkmark-circle-outline': CheckCircle, + // Close circle outline + 'close-circle-outline': XCircle, + // Help + 'help-outline': HelpCircle, + // Chat + 'chatbubbles-outline': MessageCircle, + // Information circle (without -outline) + 'information-circle': Info, + // Sync / Refresh + 'sync-outline': RefreshCw, + 'sync': RefreshCw, + // Chat bubble + 'chatbubble-ellipses-outline': MessageCircle, + 'chatbubble-outline': MessageCircle, + // Card (without -outline) + 'card': CreditCard, + // Mail (without -outline) + 'mail-outline': Mail, + // At / Username + 'at-outline': AtSign, + // Location + 'location-outline': MapPin, + // Gender + 'male-female-outline': Users, + 'man-outline': User, + 'woman-outline': User, + // Barcode + 'barcode-outline': Barcode, + // Close (outline variant) + 'close-outline': X, + // Flag + 'flag-outline': Flag, + // Arrow forward outline + 'arrow-forward-outline': ArrowRight, + // Code + 'code-working-outline': Code, + // Document text + 'document-text-outline': FileText, + // Tag / Pricetag + 'pricetag-outline': Tag, + // Search outline + 'search-outline': Search, + // Send + 'send-outline': Send, +}; + +export type LucideIconName = keyof typeof iconMap; + +type LucideIconProps = { + name: string; + size?: number; + color?: string; + style?: any; + strokeWidth?: number; +}; + +const LucideIcon: React.FC = ({ + name, + size = 24, + color = '#000', + style, + strokeWidth = 2, +}) => { + const IconComponent = iconMap[name] || Circle; + return ; +}; + +export default LucideIcon; diff --git a/src/components/common/SkeletonLoader.tsx b/src/components/common/SkeletonLoader.tsx new file mode 100644 index 0000000..c4e438b --- /dev/null +++ b/src/components/common/SkeletonLoader.tsx @@ -0,0 +1,65 @@ +import React, { useEffect, useRef } from 'react'; +import { Animated, StyleSheet, ViewStyle, DimensionValue } from 'react-native'; +import { useTheme } from '../../hooks/ui/useTheme'; + +interface SkeletonProps { + width?: DimensionValue; + height?: DimensionValue; + borderRadius?: number; + style?: ViewStyle; +} + +const SkeletonLoader: React.FC = ({ + width = '100%', + height = 20, + borderRadius = 4, + style, +}) => { + const theme = useTheme(); + const opacity = useRef(new Animated.Value(0.3)).current; + + useEffect(() => { + const animation = Animated.loop( + Animated.sequence([ + Animated.timing(opacity, { + toValue: 0.7, + duration: 800, + useNativeDriver: true, + }), + Animated.timing(opacity, { + toValue: 0.3, + duration: 800, + useNativeDriver: true, + }), + ]) + ); + + animation.start(); + + return () => animation.stop(); + }, [opacity]); + + return ( + + ); +}; + +const styles = StyleSheet.create({ + skeleton: { + overflow: 'hidden', + }, +}); + +export default SkeletonLoader; diff --git a/src/hooks/core/apiDebug.ts b/src/hooks/core/apiDebug.ts new file mode 100644 index 0000000..405b988 --- /dev/null +++ b/src/hooks/core/apiDebug.ts @@ -0,0 +1,69 @@ +import { AxiosError } from 'axios'; + +type ApiErrorPayload = { + message?: string; + error?: string; + status?: string; + statusCode?: number; + data?: unknown; +}; + +export const logGroupRequest = (method: string, url: string, payload?: unknown) => { + console.log(`[HOOK_API][${method}] URL:`, url); + console.log(`[HOOK_API][${method}] PAYLOAD:`, payload ?? null); +}; + +export const logGroupResponse = (method: string, url: string, response: unknown) => { + const meta = response as { statusCode?: number; status?: string; message?: string }; + console.log(`[HOOK_API][${method}] SUCCESS URL:`, url); + console.log(`[HOOK_API][${method}] SUCCESS META:`, { + statusCode: meta?.statusCode ?? null, + status: meta?.status ?? null, + message: meta?.message ?? null, + }); + console.log(`[HOOK_API][${method}] SUCCESS RESPONSE:`, response); +}; + +export const logGroupError = (method: string, url: string, error: unknown) => { + const axiosError = error as AxiosError; + console.error( + `[HOOK_API][${method}] ERROR RESPONSE:`, + axiosError?.response?.data ?? axiosError?.message ?? error + ); +}; + +export const getApiErrorMessage = (error: unknown) => { + const axiosError = error as AxiosError; + const apiMessage = + axiosError?.response?.data?.message || + axiosError?.response?.data?.error || + axiosError?.message; + + return apiMessage || 'An unexpected error occurred'; +}; + +export const normalizeApiError = (error: unknown) => { + const normalized = new Error(getApiErrorMessage(error)); + (normalized as Error & { originalError?: unknown }).originalError = error; + return normalized; +}; + +export const resolveMutationRequest = (resource: string, payload: T) => { + const rawPayload = payload as unknown as { resource?: string; payload?: unknown }; + const canUseDynamicResource = + (!resource || resource.trim() === '') && + typeof rawPayload?.resource === 'string' && + rawPayload.resource.trim() !== ''; + + if (canUseDynamicResource) { + return { + url: rawPayload.resource as string, + body: rawPayload.payload ?? payload, + }; + } + + return { + url: resource, + body: payload, + }; +}; diff --git a/src/hooks/core/useCreate.ts b/src/hooks/core/useCreate.ts new file mode 100644 index 0000000..2422ba3 --- /dev/null +++ b/src/hooks/core/useCreate.ts @@ -0,0 +1,38 @@ +import { useMutation, UseMutationOptions, useQueryClient } from '@tanstack/react-query'; +import apiClient from '../../api/client'; +import { + normalizeApiError, + resolveMutationRequest, +} from './apiDebug'; + +export const useCreate = ( + resource: string, + options?: UseMutationOptions +) => { + const queryClient = useQueryClient(); + + return useMutation({ + ...options, + mutationFn: async (payload: T) => { + const { url, body } = resolveMutationRequest(resource, payload); + + try { + const { data } = await apiClient.post(url, body); + return data; + } catch (error) { + throw normalizeApiError(error); + } + }, + onSuccess: (data, variables, onMutateResult, context) => { + const { url } = resolveMutationRequest(resource, variables); + queryClient.invalidateQueries({ queryKey: [url] }); + if (resource && resource !== url) { + queryClient.invalidateQueries({ queryKey: [resource] }); + } + options?.onSuccess?.(data, variables, onMutateResult, context); + }, + onError: (error, variables, onMutateResult, context) => { + options?.onError?.(error, variables, onMutateResult, context); + }, + }); +}; diff --git a/src/hooks/core/useDelete.ts b/src/hooks/core/useDelete.ts new file mode 100644 index 0000000..33089bd --- /dev/null +++ b/src/hooks/core/useDelete.ts @@ -0,0 +1,31 @@ +import { useMutation, UseMutationOptions, useQueryClient } from '@tanstack/react-query'; +import apiClient from '../../api/client'; +import { normalizeApiError } from './apiDebug'; + +export const useDelete = ( + resource: string, + options?: UseMutationOptions +) => { + const queryClient = useQueryClient(); + + return useMutation({ + ...options, + mutationFn: async (id: string | number) => { + const url = `${resource}/${id}`; + + try { + const { data } = await apiClient.delete(url); + return data; + } catch (error) { + throw normalizeApiError(error); + } + }, + onSuccess: (data, variables, onMutateResult, context) => { + queryClient.invalidateQueries({ queryKey: [resource] }); + options?.onSuccess?.(data, variables, onMutateResult, context); + }, + onError: (error, variables, onMutateResult, context) => { + options?.onError?.(error, variables, onMutateResult, context); + }, + }); +}; diff --git a/src/hooks/core/useFetch.ts b/src/hooks/core/useFetch.ts new file mode 100644 index 0000000..ba313af --- /dev/null +++ b/src/hooks/core/useFetch.ts @@ -0,0 +1,22 @@ +import { useQuery, UseQueryOptions } from '@tanstack/react-query'; +import apiClient from '../../api/client'; +import { normalizeApiError } from './apiDebug'; + +export const useFetch = ( + resource: string, + params?: object, + options?: Omit, 'queryKey' | 'queryFn'> +) => { + return useQuery({ + queryKey: [resource, params], + queryFn: async () => { + try { + const { data } = await apiClient.get(resource, { params }); + return data; + } catch (error) { + throw normalizeApiError(error); + } + }, + ...options, + }); +}; diff --git a/src/hooks/core/useInvite.ts b/src/hooks/core/useInvite.ts new file mode 100644 index 0000000..ad27cfc --- /dev/null +++ b/src/hooks/core/useInvite.ts @@ -0,0 +1,21 @@ +import { useMutation, UseMutationOptions } from '@tanstack/react-query'; +import apiClient from '../../api/client'; +import { normalizeApiError } from './apiDebug'; + +/** + * Hook to generate a group invite link. + * Returns the mutation result which includes the API response data. + */ +export const useInvite = (options?: UseMutationOptions) => { + return useMutation({ + ...options, + mutationFn: async (groupId: string | number) => { + try { + const { data } = await apiClient.get(`invite/generate?groupId=${groupId}`); + return data; + } catch (error) { + throw normalizeApiError(error); + } + }, + }); +}; diff --git a/src/hooks/core/useUpdate.ts b/src/hooks/core/useUpdate.ts new file mode 100644 index 0000000..7b4e403 --- /dev/null +++ b/src/hooks/core/useUpdate.ts @@ -0,0 +1,38 @@ +import { useMutation, UseMutationOptions, useQueryClient } from '@tanstack/react-query'; +import apiClient from '../../api/client'; +import { + normalizeApiError, + resolveMutationRequest, +} from './apiDebug'; + +export const useUpdate = ( + resource: string, + options?: UseMutationOptions +) => { + const queryClient = useQueryClient(); + + return useMutation({ + ...options, + mutationFn: async (payload: T) => { + const { url, body } = resolveMutationRequest(resource, payload); + + try { + const { data } = await apiClient.put(url, body); + return data; + } catch (error) { + throw normalizeApiError(error); + } + }, + onSuccess: (data, variables, onMutateResult, context) => { + const { url } = resolveMutationRequest(resource, variables); + queryClient.invalidateQueries({ queryKey: [url] }); + if (resource && resource !== url) { + queryClient.invalidateQueries({ queryKey: [resource] }); + } + options?.onSuccess?.(data, variables, onMutateResult, context); + }, + onError: (error, variables, onMutateResult, context) => { + options?.onError?.(error, variables, onMutateResult, context); + }, + }); +}; diff --git a/src/hooks/ui/useTheme.ts b/src/hooks/ui/useTheme.ts new file mode 100644 index 0000000..bc28a61 --- /dev/null +++ b/src/hooks/ui/useTheme.ts @@ -0,0 +1,7 @@ +import { useTheme as useThemeContext } from '../../theme/ThemeProvider'; +import { Theme } from '../../theme/theme'; + +export const useTheme = (): Theme => { + const { theme } = useThemeContext(); + return theme; +}; diff --git a/src/navigation/AppNavigator.tsx b/src/navigation/AppNavigator.tsx index 13ea7cd..de11043 100644 --- a/src/navigation/AppNavigator.tsx +++ b/src/navigation/AppNavigator.tsx @@ -24,6 +24,7 @@ import Notification from "../screens/notification/Notification"; import ManagementScreen from "../screens/management/ManagementScreen"; import { navigationRef } from "../utils/navigationService"; import NotificationRedirection from "../screens/NotificationRedirection"; +import PaymentModeScreen from "../screens/payment/PaymentModeScreen"; // import screen from "../components/test"; export type RootStackParamList = { @@ -40,13 +41,14 @@ export type RootStackParamList = { BecomeOrg: undefined; YourContacts: undefined; Transaction: undefined; - EditBankDetails: { currentBankName: string | null; currentAccountNumber: string | null; currentBranchCode: string | null; onUpdate:()=>void }; + EditBankDetails: { currentBankName: string | null; currentAccountNumber: string | null; currentBranchCode: string | null; onUpdate: () => void }; ManageJoinedGroup: { groupId: string | number }; Invite: { groupId: string | number; code: string | number }; ManagementTabs: { groupId: string | number; } TransactionList: undefined RandomSelectUserScreen: { groupId: string | number, cycleNumber: number }; Notification: undefined; + PaymentMode: { groupId: string | number; amount: number; payableData?: any }; NotificationRedirection: { data: any }; Management: | { actionTab: "My Groups" } @@ -119,8 +121,9 @@ export default function AppNavigator() { + ); } -; \ No newline at end of file +; diff --git a/src/navigation/BottomTabs.tsx b/src/navigation/BottomTabs.tsx index 8f4e9e5..288cb3c 100644 --- a/src/navigation/BottomTabs.tsx +++ b/src/navigation/BottomTabs.tsx @@ -1,50 +1,22 @@ -import React, { useState, useEffect } from "react"; +import React from "react"; import { BottomTabNavigationProp, createBottomTabNavigator, } from "@react-navigation/bottom-tabs"; import { View, - Text, StyleSheet, - Platform, - AccessibilityInfo, } from "react-native"; -import { Ionicons, IoniconsIconName } from "@react-native-vector-icons/ionicons"; -import GroupScreen from "../screens/GroupScreen"; -import Profile from "../screens/profile/Profile"; -import Home from "../screens/home/Home"; -import colors from "../components/colors"; -import Setting from "../screens/setting/Setting"; -import ManagementScreen from "../screens/management/ManagementScreen"; -import { - responsiveFontSize, - responsiveHeight, -} from "react-native-responsive-dimensions"; -import { useTheme } from "../theme/ThemeProvider"; +import { Home, User, Settings, Users } from "lucide-react-native"; import { useSafeAreaInsets } from "react-native-safe-area-context"; +import { responsiveFontSize } from "react-native-responsive-dimensions"; -const TAB_CONFIG = { - activeColor: "#000", - inactiveColor: "#8e8e93", - backgroundColor: "#fff", - height: 70, - iconSize: 28, -}; - -const activeTabColors: { [key: string]: string } = { - Home: colors.pastelPink, - Profile: colors.pastelBlue, - Group: colors.pastelGreen, - Management: colors.pastelOrange, -}; - -const activeIconColors: { [key: string]: string } = { - Home: "#333", - Profile: "#333", - Group: "#333", - Management: "#333", -}; +import HomeScreen from "../screens/home/Home"; +import Profile from "../screens/profile/Profile"; +import GroupScreen from "../screens/GroupScreen"; +import ManagementScreen from "../screens/management/ManagementScreen"; +import { useTheme } from "../hooks/ui/useTheme"; +import FontsFamily from "../constents/FontsFamily"; export type RootTabParamList = { Home: undefined; @@ -59,87 +31,72 @@ export type TabNavigationProp = BottomTabNavigationProp; const Tab = createBottomTabNavigator(); const BottomTabs: React.FC = () => { - const { theme } = useTheme(); + const theme = useTheme(); const insets = useSafeAreaInsets(); - const [isReduceMotionEnabled, setIsReduceMotionEnabled] = - useState(false); - const [isScreenReaderEnabled, setIsScreenReaderEnabled] = - useState(false); - - useEffect(() => { - AccessibilityInfo.isScreenReaderEnabled().then(setIsScreenReaderEnabled); - AccessibilityInfo.isReduceMotionEnabled().then(setIsReduceMotionEnabled); - - const screenReaderSubscription = AccessibilityInfo.addEventListener( - "screenReaderChanged", - setIsScreenReaderEnabled - ); - - return () => screenReaderSubscription.remove(); - }, []); - return ( ({ headerShown: false, - tabBarStyle: [ - [styles.tabBar,{ backgroundColor: theme.colors.reverseBackground }], - { - bottom: insets.bottom + 10 - }, - ], - - // Make each tab item occupy the full bar height and center its content + tabBarShowLabel: true, + tabBarStyle: { + position: "absolute", + bottom: Math.max(insets.bottom, 8), + left: 12, + right: 12, + height: 64, + borderRadius: 18, + backgroundColor: theme.colors.surface, + borderWidth: 1, + borderColor: theme.colors.border, + borderTopWidth: 0, + elevation: 8, + shadowColor: "#000", + shadowOffset: { width: 0, height: 4 }, + shadowOpacity: 0.12, + shadowRadius: 8, + paddingBottom: 0, + }, tabBarItemStyle: { - height: "100%", + height: 64, justifyContent: "center", alignItems: "center", - padding: 0, - margin: 0, + paddingVertical: 6, }, + tabBarActiveTintColor: theme.colors.primary, + tabBarInactiveTintColor: theme.colors.textMuted, + tabBarLabelStyle: { + fontFamily: FontsFamily.nunitoBold, + fontSize: responsiveFontSize(1.2), + marginTop: -2, + marginBottom: 4, + }, + tabBarIcon: ({ focused, color }) => { + const iconColor = focused ? theme.colors.primary : color; + const iconSize = 22; + let IconComponent: React.FC = Home; - tabBarLabel: ({ focused, color }) => - !focused ? ( - {route.name} - ) : null, - - tabBarIcon: ({ focused }) => { - let iconName: IoniconsIconName = "home-outline"; - - if (route.name === "Home") - iconName = focused ? "home" : "home-outline"; - else if (route.name === "Profile") - iconName = focused ? "person" : "person-outline"; - else if (route.name === "Management") - iconName = focused ? "settings" : "settings-outline"; - else if (route.name === "Group") - iconName = focused ? "people" : "people-outline"; - - const activeBackgroundColor = theme.colors.background; - const activeIconColor = activeIconColors[route.name]; + if (route.name === "Home") IconComponent = Home; + else if (route.name === "Profile") IconComponent = User; + else if (route.name === "Management") IconComponent = Settings; + else if (route.name === "Group") IconComponent = Users; return ( - - + ); }, })} > - + @@ -148,47 +105,13 @@ const BottomTabs: React.FC = () => { }; const styles = StyleSheet.create({ - screen: { - flex: 1, + iconWrapper: { + width: 40, + height: 28, justifyContent: "center", alignItems: "center", - backgroundColor: "#f5f7fa", - }, - text: { - fontSize: responsiveFontSize(1.8), - fontWeight: "600", - color: "#111", - }, - tabBar: { - position: "absolute", - left: 20, - right: 20, - height: TAB_CONFIG.height, - borderRadius: 20, - shadowColor: "#000", - shadowOpacity: 0.1, - shadowOffset: { width: 0, height: 5 }, - shadowRadius: 8, - elevation: 10, - paddingTop: 0, - marginHorizontal: 10, - }, - label: { - paddingVertical: 5, - fontSize: responsiveFontSize(1.2), - }, - iconContainer: { - width: 60, - height: 50, - justifyContent: "center", - alignItems: "center", - borderRadius: 15, - }, - activeIconContainer: { - elevation: 2, - shadowColor: "#000", - shadowOpacity: 0.1, - shadowRadius: 5, + borderRadius: 10, + marginBottom: 2, }, }); diff --git a/src/payment/webView.tsx b/src/payment/webView.tsx new file mode 100644 index 0000000..e69de29 diff --git a/src/screens/ContributionScreen.tsx b/src/screens/ContributionScreen.tsx index b35630a..542e8a5 100644 --- a/src/screens/ContributionScreen.tsx +++ b/src/screens/ContributionScreen.tsx @@ -1,15 +1,16 @@ -import React, { useEffect, useRef } from "react"; +import React, { useEffect, useRef } from 'react'; import { View, Text, StyleSheet, Animated, Easing, -} from "react-native"; -import { Ionicons } from "@react-native-vector-icons/ionicons"; -import colors from "../components/colors"; -import CustomModal from "../components/common/CustomModal"; -import { responsiveFontSize } from "react-native-responsive-dimensions"; +} from 'react-native'; +import LucideIcon from '../components/common/LucideIcon'; +import CustomModal from '../components/common/CustomModal'; +import { responsiveFontSize } from 'react-native-responsive-dimensions'; +import { useTheme } from '../hooks/ui/useTheme'; +import FontsFamily from '../constents/FontsFamily'; interface ContributionProps { data: any; @@ -24,137 +25,127 @@ const ContributionDetails: React.FC = ({ onClose, onConfirm, }) => { + const theme = useTheme(); + // Animations const fadeAnim = useRef(new Animated.Value(0)).current; - const slideAnim = useRef(new Animated.Value(50)).current; - const scaleAnim = useRef(new Animated.Value(0.9)).current; + const slideAnim = useRef(new Animated.Value(20)).current; useEffect(() => { if (visible) { Animated.parallel([ Animated.timing(fadeAnim, { toValue: 1, - duration: 600, + duration: 400, useNativeDriver: true, }), Animated.timing(slideAnim, { toValue: 0, - duration: 600, - easing: Easing.out(Easing.exp), - useNativeDriver: true, - }), - Animated.spring(scaleAnim, { - toValue: 1, - friction: 6, + duration: 400, + easing: Easing.out(Easing.quad), useNativeDriver: true, }), ]).start(); } else { fadeAnim.setValue(0); - slideAnim.setValue(50); - scaleAnim.setValue(0.9); + slideAnim.setValue(20); } }, [visible]); return ( - - {/* Header */} - - - Contribution Summary - + + + {/* Info Card */} + + + + ₹{data?.totalPayable || data?.baseAmount} + - {/* Contribution Info Card */} - - Base Amount - ₹{data?.baseAmount} - { - data?.alreadyPaid ? Already Paid Amount : - - Total Payable - ₹{data?.totalPayable} - } + + + + + + + - - Due Date - {data?.dueDate} - - Cycle End - {data?.cycleEnd} - - - {/* Extra Details */} - - Cycle Number - {data?.cycleNumber} - - Late Fee Mode - {data?.lateFeeMode} - - Late Fee Value - ₹{data?.lateFeeValue} - - Effective Late Fee - ₹{data?.effectiveLateFee} + {/* Status Card */} + + Cycle {data?.cycleNumber} + {data?.alreadyPaid && ( + Payment Already Completed + )} + ); }; +const InfoItem = ({ label, value }: { label: string; value: string | number }) => { + const theme = useTheme(); + return ( + + {label} + {value} + + ); +}; + export default ContributionDetails; const styles = StyleSheet.create({ - header: { - flexDirection: "row", - alignItems: "center", - marginBottom: 24, - }, - headerTitle: { - fontSize: responsiveFontSize(2.2), - fontWeight: "700", - marginLeft: 10, - }, card: { - backgroundColor: "#fff", - padding: 16, + padding: 20, borderRadius: 16, - marginBottom: 16, - shadowColor: "#000", - shadowOpacity: 0.08, - shadowOffset: { width: 0, height: 3 }, - shadowRadius: 6, - elevation: 3, + borderWidth: 1, + marginBottom: 12, + }, + headerRow: { + flexDirection: 'row', + alignItems: 'center', + gap: 12, + marginBottom: 20, + }, + amount: { + fontSize: responsiveFontSize(2.8), + fontFamily: FontsFamily.nunitoBold, + }, + infoGrid: { + flexDirection: 'row', + flexWrap: 'wrap', + gap: 16, + }, + infoItem: { + width: '45%', + }, + miniCard: { + padding: 16, + borderRadius: 12, + borderWidth: 1, + flexDirection: 'row', + justifyContent: 'space-between', + alignItems: 'center', }, label: { - fontSize: responsiveFontSize(1.3), - color: "#6b7280", - marginTop: 6, + fontSize: responsiveFontSize(1.2), + fontFamily: FontsFamily.nunitoMedium, + textTransform: 'uppercase', + letterSpacing: 0.5, }, value: { - fontSize: responsiveFontSize(1.5), - fontWeight: "500", - color: "#374151", - marginBottom: 6, + fontSize: responsiveFontSize(1.6), + fontFamily: FontsFamily.nunitoBold, + marginTop: 2, }, - valueHighlight: { - fontSize: responsiveFontSize(1.8), - fontWeight: "700", - color: colors.black, - marginBottom: 10, + statusText: { + fontSize: responsiveFontSize(1.4), + fontFamily: FontsFamily.nunitoBold, }, }); diff --git a/src/screens/GroupCreation.tsx b/src/screens/GroupCreation.tsx index 229aeac..9603a48 100644 --- a/src/screens/GroupCreation.tsx +++ b/src/screens/GroupCreation.tsx @@ -1,4 +1,4 @@ -import React, { useState } from "react"; +import React, { useState } from 'react'; import { View, Text, @@ -7,37 +7,64 @@ import { KeyboardAvoidingView, Platform, TouchableOpacity, - Alert, - Image, ToastAndroid, -} from "react-native"; -import { useNavigation } from "@react-navigation/native"; -import { CustomInput } from "../components/common/Custom"; -import CustomButton from "../components/CustomButton"; -import colors from "../components/colors"; -import { launchImageLibrary } from "react-native-image-picker"; -import Ionicons from "@react-native-vector-icons/ionicons"; -import { UPLOAD_GROUP_IMAGE } from "../utils/ApiUrls"; -import { responsiveFontSize } from "react-native-responsive-dimensions"; +} from 'react-native'; +import { useNavigation } from '@react-navigation/native'; +import { responsiveFontSize } from 'react-native-responsive-dimensions'; +import { CustomInput, CustomButton } from '../components/common/Custom'; +import { useTheme } from '../hooks/ui/useTheme'; +import FontsFamily from '../constents/FontsFamily'; +import AppView from '../components/AppView'; + +type GroupCreationPayload = { + groupName: string; + description: string; + minParticipants: number; + maxParticipants: number; + contributionAmount: number; + cycleType: string; + customCycleDays: number; + visibility: 'PUBLIC' | 'PRIVATE'; + withdrawalMethod: 'SEQUENTIAL' | 'RANDOM'; + lateFeeMode: 'DAILY' | 'MONTHLY'; + lateFeeValue: number; + entryFee: number; +}; + +type GroupCreationFormState = { + groupName: string; + description: string; + minParticipants: string; + maxParticipants: string; + contributionAmount: string; + cycleType: string; + customCycleDays: string; + visibility: 'PUBLIC' | 'PRIVATE'; + withdrawalMethod: 'SEQUENTIAL' | 'RANDOM'; + lateFeeMode: 'DAILY' | 'MONTHLY'; + lateFeeValue: string; + entryFee: string; +}; export default function GroupCreationForm() { const navigation = useNavigation(); - const [form, setForm] = useState({ - groupName: "", - description: "", - visibility: "PRIVATE", - withdrawalMethod: "RANDOM", - minParticipants: "", - maxParticipants: "", - contributionAmount: "", - cycleType: "MONTHLY", - customCycleDays: "", - lateFeeMode: "DAILY", - lateFeeValue: "", + const theme = useTheme(); + const [form, setForm] = useState({ + groupName: '', + description: '', + visibility: 'PUBLIC', + withdrawalMethod: 'SEQUENTIAL', + minParticipants: '', + maxParticipants: '', + contributionAmount: '', + cycleType: 'MONTHLY', + customCycleDays: '', + lateFeeMode: 'DAILY', + lateFeeValue: '', + entryFee: '0', }); const [errors, setErrors] = useState({}); - const [selectedImage, setSelectedImage] = useState(); const handleChange = (key: string, value: string) => { setForm({ ...form, [key]: value }); @@ -46,277 +73,351 @@ export default function GroupCreationForm() { } }; - // ---- Custom logic for Payment Cycle ---- - const cycleTypes = [ - "CUSTOM", - "DAILY", - "WEEKLY", - "MONTHLY", - "QUARTERLY", - "HALF_YEARLY", - "YEARLY", - ]; - const distributionTypes = ["SEQUENTIAL", "RANDOM"]; - const lateFeeModes = ["DAILY", "MONTHLY"]; + const distributionTypes = ['SEQUENTIAL', 'RANDOM']; + const cycleTypes = ['CUSTOM', 'DAILY', 'WEEKLY', 'MONTHLY', 'QUARTERLY', 'HALF_YEARLY', 'YEARLY']; + const lateFeeModes = ['DAILY', 'MONTHLY']; - // The core: handle payment cycle options const handleCycleChange = (val: string) => { let newForm = { ...form, cycleType: val }; - if (val === "DAILY") { - newForm = { ...newForm, cycleType: "CUSTOM", customCycleDays: "1" }; - } else if (val === "WEEKLY") { - newForm = { ...newForm, cycleType: "CUSTOM", customCycleDays: "7" }; - } else if ( - val !== "CUSTOM" - ) { - newForm = { ...newForm, customCycleDays: "" }; // Clear custom days + if (val === 'DAILY') { + newForm = { ...newForm, cycleType: 'CUSTOM', customCycleDays: '1' }; + } else if (val === 'WEEKLY') { + newForm = { ...newForm, cycleType: 'CUSTOM', customCycleDays: '7' }; + } else if (val !== 'CUSTOM') { + newForm = { ...newForm, customCycleDays: '' }; } setForm(newForm); - if (errors["customCycleDays"]) { + if (errors.customCycleDays) { setErrors({ ...errors, customCycleDays: null }); } }; const validateForm = () => { const newErrors: any = {}; - if (!form.groupName.trim()) newErrors.groupName = "Group name is required."; - if (!form.description.trim()) newErrors.description = "Description is required."; - if (!form.minParticipants || Number(form.minParticipants) <= 0) newErrors.minParticipants = "Min participants must be at least 1."; - if (!form.maxParticipants || Number(form.maxParticipants) < Number(form.minParticipants)) newErrors.maxParticipants = "Max must be greater than or equal to min."; - if (!form.contributionAmount || Number(form.contributionAmount) <= 0) newErrors.contributionAmount = "Amount must be greater than 0."; - if (form.cycleType === "CUSTOM" && (!form.customCycleDays || Number(form.customCycleDays) <= 0)) newErrors.customCycleDays = "Custom days must be greater than 0."; - if (!form.lateFeeMode && (!form.lateFeeValue || Number(form.lateFeeValue) <= 0)) newErrors.lateFeeValue = "Late fee value is required."; + const entryFeeValue = form.entryFee.trim() === '' ? 0 : Number(form.entryFee); + if (!form.groupName.trim()) newErrors.groupName = 'Group name is required.'; + if (!form.description.trim()) newErrors.description = 'Description is required.'; + if (!form.minParticipants || Number(form.minParticipants) <= 0) + newErrors.minParticipants = 'Min participants must be at least 1.'; + if (!form.maxParticipants || Number(form.maxParticipants) < Number(form.minParticipants)) + newErrors.maxParticipants = 'Max must be >= min.'; + if (!form.contributionAmount || Number(form.contributionAmount) <= 0) + newErrors.contributionAmount = 'Amount must be > 0.'; + if (Number.isNaN(entryFeeValue) || entryFeeValue < 0) + newErrors.entryFee = 'Entry fee cannot be negative.'; + if (form.cycleType === 'CUSTOM' && (!form.customCycleDays || Number(form.customCycleDays) <= 0)) + newErrors.customCycleDays = 'Custom days must be > 0.'; + if (!form.lateFeeValue || Number(form.lateFeeValue) <= 0) + newErrors.lateFeeValue = 'Late fee is required.'; setErrors(newErrors); return Object.keys(newErrors).length === 0; }; const handleReview = () => { if (validateForm()) { - const payload = { - ...form, - contributionAmount: Number(form.contributionAmount), - customCycleDays: Number(form.customCycleDays) || 0, - lateFeeValue: Number(form.lateFeeValue) || 0, - maxParticipants: Number(form.maxParticipants), + const entryFeeValue = form.entryFee.trim() === '' ? 0 : Number(form.entryFee); + const payload: GroupCreationPayload = { + groupName: form.groupName.trim(), + description: form.description.trim(), minParticipants: Number(form.minParticipants), + maxParticipants: Number(form.maxParticipants), + contributionAmount: Number(form.contributionAmount), + cycleType: form.cycleType, + customCycleDays: form.cycleType === 'CUSTOM' ? Number(form.customCycleDays) || 0 : 0, + visibility: form.visibility, + withdrawalMethod: form.withdrawalMethod, + lateFeeMode: form.lateFeeMode, + lateFeeValue: Number(form.lateFeeValue) || 0, + entryFee: Number.isNaN(entryFeeValue) ? 0 : entryFeeValue, }; - console.log('review payload',payload) - navigation.navigate("GroupReview", { form: payload }); + navigation.navigate('GroupReview', { form: payload }); } else { - ToastAndroid.show('Please fill out all required fields correctly.', ToastAndroid.SHORT); + ToastAndroid.show('Please fill required fields correctly.', ToastAndroid.SHORT); } }; return ( - - - - Create New Group - - handleChange("groupName", val)} - icon="people" - error={errors.groupName} - /> - handleChange("description", val)} - multiline - icon="document-text" - error={errors.description} - /> -
- - handleChange("visibility", val)} - /> - - - handleChange("withdrawalMethod", val)} - /> - -
-
- - - handleChange("minParticipants", val)} - keyboardType="numeric" - icon="person-add" - error={errors.minParticipants} - /> - - - handleChange("maxParticipants", val)} - keyboardType="numeric" - icon="people-circle" - error={errors.maxParticipants} - /> - + + + + + Create Circle + + Define your saving group and its rules. + -
-
- handleChange("contributionAmount", val)} - keyboardType="numeric" - icon="cash" - error={errors.contributionAmount} - /> - - + handleChange('groupName', val)} + icon="people-outline" + error={errors.groupName} /> - - {form.cycleType === "CUSTOM" && - form.customCycleDays !== "1" && - form.customCycleDays !== "7" && ( - handleChange("customCycleDays", val)} - keyboardType="numeric" - icon="calendar" - error={errors.customCycleDays} + handleChange('description', val)} + multiline + style={styles.textArea} + icon="document-text-outline" + error={errors.description} + /> + + +
+ + handleChange('visibility', val)} /> - )} -
-
- - handleChange("lateFeeMode", val)} + + + handleChange('withdrawalMethod', val)} + /> + +
+ +
+ + + handleChange('minParticipants', val)} + keyboardType="numeric" + icon="person-outline" + error={errors.minParticipants} + /> + + + handleChange('maxParticipants', val)} + keyboardType="numeric" + icon="people-outline" + error={errors.maxParticipants} + /> + + +
+ +
+ handleChange('contributionAmount', val)} + keyboardType="numeric" + icon="cash-outline" + error={errors.contributionAmount} /> - - handleChange("lateFeeValue", val)} - keyboardType="numeric" - icon="alert-circle" - error={errors.lateFeeValue} + handleChange('entryFee', val)} + keyboardType="numeric" + icon="pricetag-outline" + error={errors.entryFee} + /> + + + + {form.cycleType === 'CUSTOM' && + form.customCycleDays !== '1' && + form.customCycleDays !== '7' && ( + handleChange('customCycleDays', val)} + keyboardType="numeric" + icon="calendar-outline" + error={errors.customCycleDays} + /> + )} +
+ +
+ + handleChange('lateFeeMode', val)} + /> + + handleChange('lateFeeValue', val)} + keyboardType="numeric" + icon="alert-circle-outline" + error={errors.lateFeeValue} + /> +
+ + -
- -
-
+ + + ); } -// --- Helper Components for a cleaner layout --- -const Section: React.FC<{ title: string; children: React.ReactNode }> = ({ title, children }) => ( - - {title} - {children} - -); +const Section = ({ title, children }: { title: string; children: React.ReactNode }) => { + const theme = useTheme(); + return ( + + {title} + {children} + + ); +}; -const SettingRow: React.FC<{ label: string; children: React.ReactNode }> = ({ label, children }) => ( - - {label} - {children} - -); +const SettingRow = ({ label, children }: { label: string; children: React.ReactNode }) => { + const theme = useTheme(); + return ( + + {label} + {children} + + ); +}; -const ToggleButtons: React.FC<{ +const ToggleButtons = ({ + options, + selectedValue, + onSelect, + isWrap = false, +}: { options: string[]; selectedValue: string; onSelect: (value: string) => void; isWrap?: boolean; -}> = ({ options, selectedValue, onSelect, isWrap = false }) => ( - - {options.map((option) => ( - onSelect(option)} - style={[ - styles.toggleButton, - selectedValue === option && styles.selectedButton, - isWrap && { flex: 0, paddingHorizontal: 15 }, - ]} - > - - {option.charAt(0) + option.slice(1).toLowerCase()} - - - ))} - -); +}) => { + const theme = useTheme(); + return ( + + {options.map((option) => ( + onSelect(option)} + activeOpacity={0.7} + style={[ + styles.toggleButton, + { borderColor: theme.colors.border }, + selectedValue === option && { backgroundColor: theme.colors.primary, borderColor: theme.colors.primary }, + isWrap && { flex: 0, paddingHorizontal: 16 }, + ]} + > + + {option} + + + ))} + + ); +}; const styles = StyleSheet.create({ - container: { flexGrow: 1, padding: 16, backgroundColor: "#f5f6fa" }, - titleContainer: { marginBottom: 20, paddingHorizontal: 4 }, - title: { fontSize: responsiveFontSize(2.8), fontWeight: "bold", color: "#1F2937" }, + scrollContent: { + paddingBottom: 40, + }, + header: { + marginBottom: 24, + }, + title: { + fontSize: responsiveFontSize(2.8), + fontFamily: FontsFamily.nunitoBold, + }, + subtitle: { + fontSize: responsiveFontSize(1.6), + fontFamily: FontsFamily.nunitoRegular, + marginTop: 4, + }, section: { - backgroundColor: "#fff", - borderRadius: 12, + borderRadius: 16, + borderWidth: 1, padding: 16, marginBottom: 16, - elevation: 1, - shadowColor: "#4A5568", - shadowOpacity: 0.06, - shadowRadius: 10, }, sectionTitle: { fontSize: responsiveFontSize(1.8), - fontWeight: "600", - color: "#374151", + fontFamily: FontsFamily.nunitoBold, marginBottom: 16, - borderBottomWidth: 1, - borderBottomColor: "#F3F4F6", - paddingBottom: 10, }, settingRow: { marginBottom: 16, }, - label: { fontSize: responsiveFontSize(1.4), fontWeight: "600", marginBottom: 8, color: "#4B5563" }, - toggleContainer: { flexDirection: "row", gap: 10 }, + label: { + fontSize: responsiveFontSize(1.4), + fontFamily: FontsFamily.nunitoSemiBold, + marginBottom: 8, + }, + toggleContainer: { + flexDirection: 'row', + gap: 8, + }, toggleButton: { flex: 1, - paddingVertical: 12, + paddingVertical: 10, borderWidth: 1, - borderColor: "#D1D5DB", - borderRadius: 8, - alignItems: "center", - backgroundColor: "#F9FAFB", + borderRadius: 10, + alignItems: 'center', + justifyContent: 'center', + }, + toggleText: { + fontSize: responsiveFontSize(1.4), + fontFamily: FontsFamily.nunitoMedium, + textTransform: 'capitalize', + }, + row: { + flexDirection: 'row', + gap: 16, + }, + flex1: { + flex: 1, + }, + textArea: { + height: 100, + textAlignVertical: 'top', + }, + submitBtn: { + marginTop: 8, }, - selectedButton: { backgroundColor: colors.pastelGreen, borderColor: "#62b59a" }, - toggleButtonText: { color: "#374151", fontWeight: "600", textTransform: "capitalize" }, - selectedButtonText: { color: "#1F2937" }, - participantsRow: { flexDirection: "row", gap: 15 }, - participantInput: { flex: 1 }, }); diff --git a/src/screens/GroupReview.tsx b/src/screens/GroupReview.tsx index a5edce7..6e08035 100644 --- a/src/screens/GroupReview.tsx +++ b/src/screens/GroupReview.tsx @@ -1,20 +1,37 @@ -import React, { useState } from "react"; -import { View, Text, StyleSheet, TouchableOpacity, ScrollView, Modal, ActivityIndicator } from "react-native"; -import { Ionicons } from "@react-native-vector-icons/ionicons"; -import AppView from "../components/AppView"; -import { apiCall } from "../utils/ApiHelper"; -import { GROUP_CREATION_URL } from "../utils/ApiUrls"; -import colors from "../components/colors"; -import { responsiveFontSize } from "react-native-responsive-dimensions"; +import React, { useState } from 'react'; +import { View, Text, StyleSheet, ScrollView, Modal } from 'react-native'; +import LucideIcon from '../components/common/LucideIcon'; +import AppView from '../components/AppView'; +import { GROUP_CREATION_URL } from '../utils/ApiUrls'; +import { responsiveFontSize } from 'react-native-responsive-dimensions'; +import { useTheme } from '../hooks/ui/useTheme'; +import { useCreate } from '../hooks/core/useCreate'; +import { CustomButton } from '../components/common/Custom'; +import FontsFamily from '../constents/FontsFamily'; + +type GroupCreationPayload = { + groupName: string; + description: string; + minParticipants: number; + maxParticipants: number; + contributionAmount: number; + cycleType: string; + customCycleDays: number; + visibility: 'PUBLIC' | 'PRIVATE'; + withdrawalMethod: 'SEQUENTIAL' | 'RANDOM'; + lateFeeMode: 'DAILY' | 'MONTHLY'; + lateFeeValue: number; + entryFee: number; +}; // --- Helper: Get Cycle Label --- const getCycleLabel = (cycleType: string, customCycleDays: string | number) => { - if (cycleType === "CUSTOM") { - if (String(customCycleDays) === "1") return "Daily"; - if (String(customCycleDays) === "7") return "Weekly"; + if (cycleType === 'CUSTOM') { + if (String(customCycleDays) === '1') return 'Daily'; + if (String(customCycleDays) === '7') return 'Weekly'; return `Custom (${customCycleDays} days)`; } - return cycleType.charAt(0).toUpperCase() + cycleType.slice(1).toLowerCase().replace("_", " "); + return cycleType.charAt(0).toUpperCase() + cycleType.slice(1).toLowerCase().replace('_', ' '); }; // --- Themed Dialog Component --- @@ -31,34 +48,37 @@ const ThemedDialog = ({ onClose: () => void; onNavigate: () => void; }) => { + const theme = useTheme(); if (!visible) return null; const config = isSuccess ? { - icon: "checkmark-circle" as const, - color: "#2ecc71", - title: "Success!", - buttonText: "Go Home", - action: onNavigate, - } + icon: 'checkmark-circle' as const, + color: theme.colors.primary, + title: 'Request Submitted', + buttonText: 'Back to Home', + action: onNavigate, + } : { - icon: "close-circle" as const, - color: "#e74c3c", - title: "Oops!", - buttonText: "Try Again", - action: onClose, - }; + icon: 'close-circle' as const, + color: theme.colors.error, + title: 'Submission Failed', + buttonText: 'Review and Retry', + action: onClose, + }; return ( - - - {config.title} - {message} - - {config.buttonText} - + + + {config.title} + {message} + @@ -66,116 +86,127 @@ const ThemedDialog = ({ }; export default function GroupReview({ route, navigation }: any) { - const { form } = route.params; - const [isLoading, setIsLoading] = useState(false); - const [isSuccess, setIsSuccess] = useState(false); + const { form } = route.params as { form: GroupCreationPayload }; + const theme = useTheme(); const [modalVisible, setModalVisible] = useState(false); - const [feedbackMessage, setFeedbackMessage] = useState(""); + const [feedbackMessage, setFeedbackMessage] = useState(''); + const [isSuccess, setIsSuccess] = useState(false); + + const { mutate: createGroup, isPending: isLoading } = useCreate(GROUP_CREATION_URL); const handleNavigateHome = () => { setModalVisible(false); - navigation.reset({ index: 0, routes: [{ name: "Home" }] }); + navigation.reset({ index: 0, routes: [{ name: 'Home' }] }); }; - const handleSubmit = async () => { - setIsLoading(true); - try { - const res = (await apiCall(GROUP_CREATION_URL, "POST", form)) as { - statusCode?: number; - message?: string; - }; - if (res?.statusCode === 201) { + const handleSubmit = () => { + const payload: GroupCreationPayload = { + groupName: String(form.groupName || '').trim(), + description: String(form.description || '').trim(), + minParticipants: Number(form.minParticipants) || 0, + maxParticipants: Number(form.maxParticipants) || 0, + contributionAmount: Number(form.contributionAmount) || 0, + cycleType: String(form.cycleType || 'MONTHLY'), + customCycleDays: String(form.cycleType || '').toUpperCase() === 'CUSTOM' + ? Number(form.customCycleDays) || 0 + : 0, + visibility: (String(form.visibility || 'PUBLIC').toUpperCase() === 'PRIVATE' ? 'PRIVATE' : 'PUBLIC'), + withdrawalMethod: (String(form.withdrawalMethod || 'SEQUENTIAL').toUpperCase() === 'RANDOM' ? 'RANDOM' : 'SEQUENTIAL'), + lateFeeMode: (String(form.lateFeeMode || 'DAILY').toUpperCase() === 'MONTHLY' ? 'MONTHLY' : 'DAILY'), + lateFeeValue: Number(form.lateFeeValue) || 0, + entryFee: Number(form.entryFee) || 0, + }; + + createGroup(payload, { + onSuccess: (res) => { setIsSuccess(true); - setFeedbackMessage(res.message || "Your group has been created successfully."); - } else { - throw new Error(res?.message || "Failed to create the group."); - } - } catch (error: any) { - setIsSuccess(false); - setFeedbackMessage(error.message || "An unexpected error occurred."); - } finally { - setIsLoading(false); - setModalVisible(true); - } + setFeedbackMessage(res.message || 'Your circle request was submitted for admin approval.'); + setModalVisible(true); + }, + onError: (error) => { + setIsSuccess(false); + setFeedbackMessage(error.message || 'Failed to submit your request. Please try again.'); + setModalVisible(true); + }, + }); }; - const TimelineSection: React.FC<{ icon: any; title: string; children: React.ReactNode }> = ({ - icon, - title, - children, - }) => ( + const TimelineSection = ({ icon, title, children }: { icon: any; title: string; children: React.ReactNode }) => ( - - + + - + - {title} - {children} + {title} + {children} ); const renderRow = (label: string, value: string | number | null) => { - if (value === null || value === undefined || value === "") return null; + if (value === null || value === undefined || value === '') return null; return ( - - {label} - {value} + + {label} + {value} ); }; return ( - - + + - {form.groupName || "Group Review"} - Confirm the details to launch your group. + {form.groupName || 'Circle Review'} + + Review all details before submitting for admin approval. + - {renderRow("Name", form.groupName)} + {renderRow('Name', form.groupName)} {form.description ? ( - Description - {form.description} + Description + {form.description} ) : null} - - {renderRow("Visibility", form.visibility)} - {renderRow("Distribution", form.withdrawalMethod)} - {renderRow("Min Members", form.minParticipants)} - {renderRow("Max Members", form.maxParticipants)} + + {renderRow('Visibility', form.visibility)} + {renderRow('Distribution', form.withdrawalMethod)} + {renderRow('Min Members', form.minParticipants)} + {renderRow('Max Members', form.maxParticipants)} - - {renderRow("Contribution", `₹${form.contributionAmount}`)} - {renderRow("Cycle", getCycleLabel(form.cycleType, form.customCycleDays))} - {/* Show days only if neither Daily nor Weekly */} - {form.cycleType === "CUSTOM" && !["1", "7"].includes(String(form.customCycleDays)) - && renderRow("Cycle Days", `${form.customCycleDays} days`)} + + {renderRow('Contribution', `₹${form.contributionAmount}`)} + {renderRow('Entry Fee', `₹${form.entryFee}`)} + {renderRow('Cycle', getCycleLabel(form.cycleType, form.customCycleDays))} + {form.cycleType === 'CUSTOM' && !['1', '7'].includes(String(form.customCycleDays)) && + renderRow('Cycle Days', `${form.customCycleDays} days`)} - - {renderRow("Fee Mode", form.lateFeeMode)} - {renderRow("Fee Value", `₹${form.lateFeeValue}`)} + + {renderRow('Fee Mode', form.lateFeeMode)} + {renderRow('Fee Value', `₹${form.lateFeeValue}`)} - - {isLoading ? ( - - ) : ( - <> - - Create Group - - )} - + void; onView: (group: Group) => void; isGrid?: boolean; }) => { - const isFull = item.currentParticipants >= item.maxParticipants; - const cardStyle = isGrid ? styles.gridCard : styles.listCard; - const [loading, setLoading] = useState(false) - const navigation = useNavigation() +const PUBLIC_GROUPS_PARAMS = { + page: 0, + size: 10, +}; +const getGroupsFromResponse = (response: any): Group[] => { + if (!response) return []; + if (Array.isArray(response?.data?.content)) return response.data.content; + if (Array.isArray(response?.data)) return response.data; + return []; +}; + +// --- UI Components --- +const GroupCard = ({ + item, + onJoin, + isJoined, + isJoining +}: { + item: Group; + onJoin: () => void; + isJoined: boolean; + isJoining: boolean; +}) => { + const theme = useTheme(); + const isFull = item.currentParticipants >= item.maxParticipants; + const isPending = Boolean(item.joinRequestPending); + const canJoin = !isJoined && !isPending && !isFull; + console.log("item>>>>>>>>>>>>>>", item); return ( - - {/* navigation.navigate("GroupDetails" as never)}> */} - - - {item.groupName} - {item.description} + + + + + {item.groupName} + + + {item.description} + - - {item.currentParticipants}/{item.maxParticipants} Members - {item.cycleType} - - {/* */} - - {/* onView(item)}> - View - */} - { - setLoading(true); - await onJoin(item.id); - setLoading(false); - }} - disabled={isFull || loading} // Also disable button while loading - > - {loading ? : {isFull ? "Full" : "Join"}} - + + + + + + + {item.currentParticipants}/{item.maxParticipants} + + + + + + {item.cycleType === 'CUSTOM' && item.customCycleDays + ? (item.customCycleDays === 1 ? 'Daily' : item.customCycleDays === 7 ? 'Weekly' : `${item.customCycleDays} Days`) + : item.cycleType} + + + {item.entryFee && item.entryFee > 0 ? ( + + + + ₹{item.entryFee} + + + ) : null} + + {!isJoined && ( + 0 ? 'Pay & Join' : 'Join Circle')} + onPress={onJoin} + disabled={!canJoin || isJoining} + loading={isJoining} + style={styles.joinBtn} + type={!canJoin ? 'secondary' : 'primary'} + /> + )} ); }; -const FeaturedGroupCard = ({ item }: { item: Group }) => { - return ( - - - - {item.groupName} - by {item.createdByName} - - - ) -}; - export default function GroupScreen() { - const [activeTab, setActiveTab] = useState("Public"); - const [searchQuery, setSearchQuery] = useState(""); - const [isGridView, setIsGridView] = useState(false); - const navigation = useNavigation>(); + const theme = useTheme(); + const navigation = useNavigation(); + const insets = useSafeAreaInsets(); + const userRole = useSelector((state: any) => state.user?.role); + const [activeTab, setActiveTab] = useState<'Public' | 'Joined'>('Public'); - const [myGroups, setMyGroups] = useState([]); - const [joinedGroups, setJoinedGroups] = useState([]); - const [publicGroups, setPublicGroups] = useState([]); - const [featuredGroups, setFeaturedGroups] = useState([]); - const [loading, setLoading] = useState(false); - const [error, setError] = useState(null); - const [role, setRole] = useState(null); - const [refreshing, setRefreshing] = useState(false) - const [alertInfo, setAlertInfo] = useState({ - visible: false, - title: '', - message: '', - buttons: [] as { text: string; onPress: () => void; style?: 'cancel' | 'destructive' | 'default' }[], - alertType: 'info' as 'info' | 'success' | 'error', + const [searchQuery, setSearchQuery] = useState(''); + const [joiningGroupId, setJoiningGroupId] = useState(null); + + // Data Fetching + const { data: publicGroupsRes, isLoading: publicLoading, refetch: refetchPublic } = useFetch( + PUBLIC_GROUPS_URL, + PUBLIC_GROUPS_PARAMS + ); + const { data: joinedGroupsRes, isLoading: joinedLoading, refetch: refetchJoined } = useFetch(GET_JOINED_GROUP_URL); + const { data: userStatusRes } = useFetch(USER_STATUS); + const { mutate: joinGroup, isPending: isJoining } = useCreate(''); + + const role = String(userStatusRes?.data?.role || userRole || '').trim().toUpperCase(); + const isOrganizer = role === 'ORGANIZER'; + + const publicGroups = useMemo(() => { + const groups = getGroupsFromResponse(publicGroupsRes); + return groups.filter((g) => { + const visibility = String(g.visibility || "").toUpperCase(); + const status = String(g.status || "").toUpperCase(); + return visibility === "PUBLIC" && status === "APPROVED"; }); + }, [publicGroupsRes]); - const fetchGroups = async () => { - if (loading) return; - setLoading(true); - setError(null); - try { - const res = await apiCall(PUBLIC_GROUPS_URL, "GET") as { data?: Group[] }; - if (res && res.data) { - setPublicGroups(res.data); - setFeaturedGroups(res.data.slice(0, 5)); - } else { - throw new Error("No data received"); - } - } catch (e) { - setError("Failed to fetch groups. Please try again."); - } finally { - setLoading(false); - } - }; + const joinedGroups = useMemo(() => getGroupsFromResponse(joinedGroupsRes), [joinedGroupsRes]); - - const fetchJoinedGroups = async () => { - if (loading) return; - setLoading(true); - setError(null); - try { - console.log("first-????-") - const res = await apiCall(GET_JOINED_GROUP_URL, "GET") as { data?: Group[] }; - console.log("first---", res) - if (res && res.data) { - setJoinedGroups(res.data); - - } else { - throw new Error("No data received"); - } - } catch (e) { - setError("Failed to fetch groups. Please try again."); - } finally { - setLoading(false); - } - }; - - useFocusEffect(React.useCallback(() => { - fetchGroups() - fetchJoinedGroups() - }, [])); - - const filterData = (data: Group[]) => { + const filteredData = useMemo(() => { + const data = activeTab === 'Public' ? publicGroups : joinedGroups; if (!searchQuery.trim()) return data; - return data.filter(g => - g.groupName.toLowerCase().includes(searchQuery.toLowerCase()) - ); - }; + return data.filter((g: Group) => String(g.groupName || "").toLowerCase().includes(searchQuery.toLowerCase())); + }, [activeTab, joinedGroups, publicGroups, searchQuery]); - const handleJoinGroup = async (groupId: string, userId: string) => { - console.log("Joining group:", groupId, "User:", userId); - try { - const url = `${JOIN_GROUP_URL}groups/${groupId}/join/10`; - console.log("Joining group URL:", url); - const res = await apiCall(url, "POST") as any; + const [refreshing, setRefreshing] = useState(false); + const onRefresh = useCallback(() => { + setRefreshing(true); + const promises = [refetchPublic(), refetchJoined()]; + Promise.all(promises).finally(() => setRefreshing(false)); + }, [refetchPublic, refetchJoined]); - console.log("JOIN_GROUP_URL response:", res); - if (res.statusCode === 200) { - setAlertInfo({ - visible: true, - title: 'Success', - message: 'Your request to join the group has been sent.', - buttons: [ - { text: 'OK', onPress: () => setAlertInfo({ ...alertInfo, visible: false }), buttonColor: "#28a745", textColor: "#fff" } - ], - alertType: 'success', - }); - } else { - ToastAndroid.show(res.message || "Failed to join group.", ToastAndroid.SHORT); - } - } catch (error) { - console.error("Join Group Error:", error); - } - }; - - const userStatus = async () => { - try { - const credentials = await Keychain.getGenericPassword(); - if (!credentials) { - ToastAndroid.show('You are not logged in', ToastAndroid.SHORT); - - return; - } - const token = credentials.password; - - setLoading(true); - setError(null); - const response = await fetch(USER_STATUS, { - method: "GET", - headers: { - Accept: "*/*", - Authorization: `Bearer ${token}`, - }, - }); - - const json: ApiResponse = await response.json(); - - console.log("👤 Profile Status Response:", json); - if (json.status === "success") { - setRole(json.data.role); - - if (json.data.profileStatus === false) { - - } - } else { - setError(json.message || "Failed to fetch status"); - } - } catch (err) { - setError("Something went wrong. Please try again."); - } finally { - setLoading(false); - } - }; useFocusEffect( useCallback(() => { - userStatus(); + onRefresh(); }, []) ); - - - const handleViewGroup = (group: Group) => { - // navigation.navigate('GroupDetails', { group }); - ToastAndroid.show(`Viewing details for ${group.groupName}`, ToastAndroid.SHORT); - }; - - const onRefresh = () => { - setRefreshing(true) - fetchGroups() - setTimeout(() => { - setRefreshing(false) - }, 20000); - } - - const renderContent = () => { - let data: Group[]; - switch (activeTab) { - case "My Groups": data = myGroups; break; - case "Joined": data = joinedGroups; break; - case "Public": data = publicGroups; break; - default: data = []; - } - - if (loading && !publicGroups.length) return ; - if (error) return {error}; - - const filteredData = filterData(data); - if (filteredData.length === 0 && !loading) return No groups found.; - - return ( - item.id} - renderItem={({ item }) => ( - { - console.log('mmmm') - if (activeTab === 'Public') { - handleJoinGroup(item.id, 'currentUserId'); - } - }} - onView={handleViewGroup} - isGrid={isGridView} - /> - )} - columnWrapperStyle={isGridView ? { justifyContent: 'space-between' } : null} - contentContainerStyle={{ paddingBottom: 150 }} - refreshControl={ - - } - // ListFooterComponent={} - /> + const handleJoin = (groupId: string | number) => { + console.log("Hitting here"); + setJoiningGroupId(groupId); + joinGroup( + { resource: `${JOIN_GROUP_URL}groups/${String(groupId)}/join/10`, payload: {} } as any, + { + onSuccess: () => { + setJoiningGroupId(null); + ToastAndroid.show('Join request sent successfully!', ToastAndroid.SHORT); + refetchPublic(); + }, + onError: (err) => { + setJoiningGroupId(null); + ToastAndroid.show(err.message || 'Failed to send request', ToastAndroid.SHORT); + }, + } ); }; + const renderSkeletons = () => ( + + {[1, 2, 3, 4].map((i) => ( + + ))} + + ); + return ( - {/* Fixed Header */} - - Explore Groups - - - + + + Explore Circles + + + + + {!isOrganizer && ( + + {(['Public', 'Joined'] as const).map((tab) => ( + setActiveTab(tab)} + style={[ + styles.tab, + activeTab === tab && { backgroundColor: theme.colors.card, borderRadius: 10, shadowColor: '#000', shadowOffset: { width: 0, height: 2 }, shadowOpacity: 0.1, shadowRadius: 4, elevation: 2 } + ]} + > + + {tab} + + + ))} + + )} - {/* Scrollable Content */} - - - - - - {role === "ORGANIZER" ? (["Public"].map((tab) => ( - setActiveTab(tab)}> - {tab} - - ))) : (["Joined", "Public"].map((tab) => ( - setActiveTab(tab)}> - {tab} - - )))} - - setIsGridView(!isGridView)} style={styles.layoutToggleButton}> - - - - - {/* The list now renders inside a simple View, as FlatList handles its own scroll */} - {renderContent()} - - {role === "ORGANIZER" && ( - } onPress={() => navigation.navigate('GroupCreation')} /> - )} - String(item.id)} + renderItem={({ item }) => ( + item?.entryFee && item?.entryFee > 0 ? navigation.navigate('PaymentMode' as any, { + groupId: item.id, + routeFrom: 'GroupScreen', + routeData: item + }) : handleJoin(item.id)} + isJoining={isJoining && joiningGroupId === item.id} + /> + )} + contentContainerStyle={styles.listContent} + refreshControl={ + + } + ListEmptyComponent={ + !publicLoading && !joinedLoading ? ( + + + + {searchQuery ? 'No circles match your search' : 'No circles found here'} + + + ) : renderSkeletons() + } /> - + + {isOrganizer && ( + } + onPress={() => navigation.navigate('GroupCreation')} + /> + )} ); } const styles = StyleSheet.create({ + topContainer: { + paddingHorizontal: 14, + paddingTop: 10, + paddingBottom: 8, + }, + header: { + flexDirection: 'row', + justifyContent: 'space-between', + alignItems: 'center', + marginBottom: 14, + }, + title: { + fontSize: responsiveFontSize(2.8), + fontFamily: FontsFamily.nunitoBold, + }, + iconButton: { + width: 38, + height: 38, + borderRadius: 12, + borderWidth: 1, + justifyContent: 'center', + alignItems: 'center', + }, + tabs: { + flexDirection: 'row', + padding: 4, + borderRadius: 14, + borderWidth: 1, + marginTop: 12, + marginBottom: 4, + }, + tab: { + flex: 1, + paddingVertical: 10, + alignItems: 'center', + justifyContent: 'center', + }, + tabText: { + fontSize: responsiveFontSize(1.6), + fontFamily: FontsFamily.nunitoSemiBold, + }, + listContent: { + paddingHorizontal: 14, + paddingBottom: 86, + }, + card: { + borderRadius: 16, + borderWidth: 1, + padding: 14, + marginBottom: 12, + }, + listCard: { + width: '100%', + }, - headerContainer: { backgroundColor: '#f5f6fa', paddingHorizontal: 16 }, - title: { fontSize: responsiveFontSize(2.2), fontWeight: "600", color: colors.Group }, - searchContainer: { flexDirection: 'row', alignItems: 'center', backgroundColor: '#fff', borderRadius: 20, paddingHorizontal: 10, elevation: 5, marginVertical: 20 , borderWidth:2,borderColor:colors.pastelGreen }, - searchIcon: { marginRight: 8 }, - searchInput: { flex: 1, height: 45, fontSize: responsiveFontSize(1.6), color: '#333' }, - sectionTitle: { fontSize: responsiveFontSize(2.0), fontWeight: 'bold', color: colors.Group, marginBottom: 12, paddingHorizontal: 16 }, - featuredList: { paddingLeft: 16, marginBottom: 20 }, - featuredCard: { width: width * 0.45, marginRight: 15, backgroundColor: '#fff', borderRadius: 12, elevation: 3, overflow: 'hidden' }, - featuredImage: { width: '100%', height: 100 }, - featuredContent: { padding: 10 }, - featuredName: { fontSize: responsiveFontSize(1.4), fontWeight: 'bold', color: '#333' }, - featuredCreator: { fontSize: responsiveFontSize(1.2), color: '#777', marginTop: 2 }, - tabHeader: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', paddingHorizontal: 16, marginBottom: 16 }, - segmentedControl: { flex: 1, flexDirection: 'row', borderRadius: 10, padding: 4, marginRight: 10 }, - segmentButton: { flex: 1, paddingVertical: 10, borderRadius: 8, alignItems: 'center' }, - segmentButtonActive: { backgroundColor: colors.pastelGreen, elevation: 5, shadowColor: '#000', shadowOpacity: 0.1, shadowRadius: 5 , borderRadius:15 }, - segmentText: { fontSize: responsiveFontSize(1.4), fontWeight: '600', color: '#555' }, - segmentTextActive: { color: '#333' }, - layoutToggleButton: { padding: 8 }, - fab: { position: "absolute", right: 20, bottom: 90, width: 60, height: 60, justifyContent: "center", alignItems: "center", backgroundColor: colors.pastelGreen, borderRadius: 30, elevation: 4 }, - listCard: { backgroundColor: colors.pastelGreen, padding: 16, borderRadius: 20, marginBottom: 12,marginHorizontal:15, elevation: 5 }, - gridCard: { backgroundColor: colors.pastelGreen, padding: 12, borderRadius: 20, marginBottom: 12,marginHorizontal:15, elevation: 5, width: (width - 48) / 2 }, - groupName: { fontSize: responsiveFontSize(1.6), fontWeight: 'bold', color: '#333', width: '50%' }, - groupDescription: { fontSize: responsiveFontSize(1.4), color: '#666', marginVertical: 4, width: '40%' }, - groupMeta: { flexDirection: 'row', justifyContent: 'space-between', marginTop: 8, borderTopWidth: 1, borderTopColor: '#f0f0f0', paddingTop: 8, marginBottom: 10 }, - groupMetaText: { fontSize: responsiveFontSize(1.2), color: '#888', fontWeight: '500', textTransform: 'capitalize' }, - emptyText: { textAlign: 'center', marginTop: 40, fontSize: responsiveFontSize(1.6) , color: '#888' }, - emptyFeaturedText: { marginLeft: 20, color: '#888' }, - buttonContainer: { flexDirection: 'row', justifyContent: 'space-between', marginTop: 8 }, - viewButton: { flex: 1, backgroundColor: '#e9ecef', paddingVertical: 10, borderRadius: 8, alignItems: 'center', marginRight: 5 }, - viewButtonText: { color: '#333', fontWeight: 'bold', fontSize: responsiveFontSize(1.4) }, - joinButton: { flex: 1, backgroundColor: colors.white, paddingVertical: 10, borderRadius: 8, alignItems: 'center', marginLeft: 5 }, - joinButtonDisabled: { backgroundColor: '#bdc3c7' }, - joinButtonText: { color: '#333', fontWeight: 'bold', fontSize: responsiveFontSize(1.4) }, + cardHeader: { + flexDirection: 'row', + justifyContent: 'space-between', + alignItems: 'flex-start', + }, + groupInfo: { + flex: 1, + marginRight: 10, + }, + groupName: { + fontSize: responsiveFontSize(1.8), + fontFamily: FontsFamily.nunitoBold, + marginBottom: 2, + }, + groupDescription: { + fontSize: responsiveFontSize(1.4), + fontFamily: FontsFamily.nunitoRegular, + }, + groupAvatar: { + width: 48, + height: 48, + borderRadius: 12, + borderWidth: 1, + }, + cardDivider: { + height: 1, + marginVertical: 10, + }, + cardMeta: { + flexDirection: 'row', + justifyContent: 'space-between', + marginBottom: 12, + }, + metaItem: { + flexDirection: 'row', + alignItems: 'center', + gap: 4, + }, + metaText: { + fontSize: responsiveFontSize(1.3), + fontFamily: FontsFamily.nunitoMedium, + }, + joinBtn: { + paddingVertical: 10, + marginVertical: 0, + borderRadius: 10, + }, + + skeletonList: { + flexDirection: 'column', + gap: 16, + }, + emptyState: { + alignItems: 'center', + marginTop: 64, + }, + emptyText: { + marginTop: 16, + fontSize: responsiveFontSize(1.6), + fontFamily: FontsFamily.nunitoMedium, + }, + fab: { + position: 'absolute', + right: 16, + borderRadius: 30, + width: 56, + height: 56, + justifyContent: 'center', + alignItems: 'center', + }, }); diff --git a/src/screens/InviteScreen.tsx b/src/screens/InviteScreen.tsx index e0a2a1a..f2679a5 100644 --- a/src/screens/InviteScreen.tsx +++ b/src/screens/InviteScreen.tsx @@ -1,21 +1,24 @@ -import { NativeStackScreenProps } from '@react-navigation/native-stack'; -import React, { useState, useEffect, useCallback, FC } from 'react'; +import React, { useEffect } from 'react'; import { View, Text, StyleSheet, ScrollView, - TouchableOpacity, - Alert, - ActivityIndicator, ToastAndroid, } from 'react-native'; -import { RootStackParamList } from '../navigation/AppNavigator'; // Adjust path -import { apiCall } from '../utils/ApiHelper'; // Adjust path -import AppView from '../components/AppView'; // Adjust path -import colors from '../components/colors'; // Adjust path +import { NativeStackScreenProps } from '@react-navigation/native-stack'; +import { RootStackParamList } from '../navigation/AppNavigator'; +import { useFetch } from '../hooks/core/useFetch'; +import { useCreate } from '../hooks/core/useCreate'; import { BASE_URL } from '../utils/ApiUrls'; +import AppView from '../components/AppView'; +import { useTheme } from '../hooks/ui/useTheme'; +import { CustomButton } from '../components/common/Custom'; +import SkeletonLoader from '../components/common/SkeletonLoader'; import { responsiveFontSize } from 'react-native-responsive-dimensions'; +import LucideIcon from '../components/common/LucideIcon'; +import * as Keychain from 'react-native-keychain'; +import FontsFamily from '../constents/FontsFamily'; // --- Type Definitions --- interface Participant { @@ -31,179 +34,301 @@ interface GroupDetails { groupName: string; description: string; contributionAmount: number; + minParticipants: number; maxParticipants: number; - cycleType: 'QUARTERLY' | 'MONTHLY' | 'WEEKLY' | 'CUSTOM'; + entryFee: number; + latePaymentCharge: number; + lateFeeMode: string; + withdrawalMethod: string; + cycleType: 'QUARTERLY' | 'MONTHLY' | 'WEEKLY' | 'CUSTOM' | 'YEARLY' | 'HALF_YEARLY'; + customCycleDays: number | null; status: 'PENDING' | 'ACTIVE' | 'COMPLETED' | 'APPROVED'; organizerFirstName: string; organizerLastName: string | null; + organizerMobile: string; + organizerEmail: string; totalParticipants: number; participants: Participant[]; } -type InviteScreenProps = NativeStackScreenProps; // Assuming 'Invite' is in your stack +type InviteScreenProps = NativeStackScreenProps; -// --- Main Screen Component --- -const InviteScreen: FC = ({ route }) => { - const { groupId,code} = route.params; - - const [groupDetails, setGroupDetails] = useState(null); - const [loading, setLoading] = useState(true); - const [joining, setJoining] = useState(false); - const [error, setError] = useState(null); - - const fetchGroupDetails = useCallback(async () => { - setLoading(true); - setError(null); - try { - const response = await apiCall(`${BASE_URL}fund/group/details?groupId=${groupId}`, 'GET') as { status: string; data?: GroupDetails; message?: string }; - if (response.status === 'success' && response.data) { - setGroupDetails(response.data); - } else { - throw new Error(response.message || 'Failed to fetch group invitation details.'); - } - } catch (e: any) { - setError(e.message); - } finally { - setLoading(false); - } - }, [groupId]); +const InviteScreen: React.FC = ({ route, navigation }) => { + const { groupId, code } = route.params; + const theme = useTheme(); useEffect(() => { - fetchGroupDetails(); - }, [fetchGroupDetails]); + const checkAuth = async () => { + try { + const credentials = await Keychain.getGenericPassword(); + if (!credentials) { + navigation.replace('Login'); + } + } catch (error) { + console.error('Auth check error:', error); + navigation.replace('Login'); + } + }; + checkAuth(); + }, [navigation]); - const handleJoinGroup = async () => { + const { data: detailsRes, isLoading, error } = useFetch( + `${BASE_URL}fund/group/details?groupId=${groupId}` + ); + console.log(detailsRes, "detailsRes") + const { mutate: joinGroup, isPending: isJoining } = useCreate( + `${BASE_URL}fund/groups/join/invite/${code}` + ); + + const groupDetails: GroupDetails | null = detailsRes?.data || null; + + const handleJoin = () => { if (!groupDetails) return; - - setJoining(true); - try { - // Replace with your actual API call to join the group - // For now, it just shows an alert. - await apiCall(`${BASE_URL}fund/groups/join/invite/${code}`, 'POST'); - ToastAndroid.show(`You have requested to join "${groupDetails.groupName}".`, ToastAndroid.SHORT); - fetchGroupDetails(); // Refresh details after joining - } catch (e: any) { - ToastAndroid.show(e.message || 'An unexpected error occurred.', ToastAndroid.SHORT); - } finally { - setJoining(false); - } + joinGroup({}, { + onSuccess: () => { + ToastAndroid.show(`Join request sent for "${groupDetails.groupName}".`, ToastAndroid.SHORT); + navigation.navigate('Home'); + }, + onError: (err: any) => { + ToastAndroid.show(err.message || 'Failed to join circle', ToastAndroid.SHORT); + }, + }); }; - if (loading) return ; - if (error) return {error}; - if (!groupDetails) return null; + if (isLoading) { + return ( + + + + + + ); + } + + if (error || !groupDetails) { + return ( + + + + Invalid Invitation + + {error instanceof Error ? error.message : 'This invitation link might be expired or invalid.'} + + navigation.navigate('Home')} type="outline" /> + + + ); + } const organizerName = `${groupDetails.organizerFirstName} ${groupDetails.organizerLastName || ''}`.trim(); + const isJoinable = groupDetails.status === 'PENDING' || groupDetails.status === 'APPROVED'; return ( - - You're Invited! - Join the group {groupDetails.groupName} + + + + + + You're Invited! + + Join {groupDetails.groupName} and start saving together. + + - {/* Group Info Card */} - - Group Details + + Circle Overview - + + + + + - + - {/* Participants Card */} - - Participants ({groupDetails.totalParticipants} / {groupDetails.maxParticipants}) - {groupDetails.participants.length > 0 ? ( - groupDetails.participants.map(p => ( - - {`${p.firstName} ${p.lastName || ''}`.trim()} - {p.role === 'ORGANIZER' && Organizer} - - )) - ) : ( - Be the first to join! - )} - - - - {/* Floating Join Button */} - - - {joining ? ( - + + + Members ({groupDetails.totalParticipants}/{groupDetails.maxParticipants}) + + {groupDetails.participants.length > 0 ? ( + groupDetails.participants.map((p) => ( + + + + + {p.firstName.charAt(0)} + + + + {`${p.firstName} ${p.lastName || ''}`.trim()} + + + {p.role === 'ORGANIZER' && ( + + Organizer + + )} + + )) ) : ( - - {groupDetails.status === 'PENDING' || groupDetails.status === 'APPROVED' ? 'Join Group' : `Group is ${groupDetails.status}`} - + Be the first to join! )} - - + + + + ); }; -// Helper component for displaying rows -const InfoRow = ({ label, value }: { label: string; value: string | number }) => ( - - {label} - {value} - -); +const InfoRow = ({ label, value }: { label: string; value: string | number }) => { + const theme = useTheme(); + return ( + + {label} + {value} + + ); +}; const styles = StyleSheet.create({ - centered: { flex: 1, justifyContent: 'center', alignItems: 'center', backgroundColor: '#F3F4F6' }, - errorText: { color: '#EF4444', fontSize: responsiveFontSize(1.2), textAlign: 'center', padding: 20 }, - contentContainer: { padding: 16, paddingBottom: 100 }, // Add padding to avoid overlap with button - - headerTitle: { fontSize: responsiveFontSize(3.2), fontWeight: 'bold', color: '#111827', textAlign: 'center' }, - headerSubtitle: { fontSize: responsiveFontSize(1.8), color: '#4B5563', textAlign: 'center', marginBottom: 24, marginTop: 4 }, - - card: { backgroundColor: '#FFFFFF', borderRadius: 16, padding: 20, marginBottom: 16, shadowColor: '#374151', shadowOpacity: 0.05, shadowRadius: 10 }, - cardTitle: { fontSize: responsiveFontSize(1.8), fontWeight: 'bold', color: '#1F2937', marginBottom: 12, borderBottomWidth: 1, borderBottomColor: '#E5E7EB', paddingBottom: 10 }, - infoRow: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', paddingVertical: 8 }, - infoLabel: { fontSize: responsiveFontSize(1.6), color: '#6B7280' }, - infoValue: { fontSize: responsiveFontSize(1.6), fontWeight: '600', color: '#111827', textTransform: 'capitalize' }, - - participantRow: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', paddingVertical: 10 }, - participantName: { fontSize: responsiveFontSize(1.6), color: '#1F2937' }, - organizerBadge: { backgroundColor: '#FEF3C7', paddingHorizontal: 8, paddingVertical: 3, borderRadius: 12 }, - organizerBadgeText: { color: '#92400E', fontSize: responsiveFontSize(1.2), fontWeight: '500' }, - noParticipantsText: { fontSize: responsiveFontSize(1.6), color: '#6B7280', textAlign: 'center', paddingVertical: 10 }, - - floatingButtonContainer: { - position: 'absolute', - bottom: 20, - left: 20, - right: 20, - alignItems: 'center', + scrollContent: { + paddingBottom: 40, }, - joinButton: { - backgroundColor: colors.primary, - borderRadius: 14, - paddingVertical: 16, - paddingHorizontal: 32, - elevation: 5, + header: { + alignItems: 'center', + marginBottom: 32, + marginTop: 16, + }, + inviteBadge: { + width: 60, + height: 60, + borderRadius: 30, + justifyContent: 'center', + alignItems: 'center', + marginBottom: 16, shadowColor: '#000', shadowOffset: { width: 0, height: 4 }, - shadowOpacity: 0.3, - shadowRadius: 4, - width: '80%', + shadowOpacity: 0.1, + shadowRadius: 8, + elevation: 4, + }, + headerTitle: { + fontSize: responsiveFontSize(3.2), + fontFamily: FontsFamily.nunitoBold, + }, + headerSubtitle: { + fontSize: responsiveFontSize(1.8), + fontFamily: FontsFamily.nunitoRegular, + textAlign: 'center', + marginTop: 8, + paddingHorizontal: 20, + lineHeight: 24, + }, + card: { + borderRadius: 16, + borderWidth: 1, + padding: 20, + marginBottom: 16, + }, + cardTitle: { + fontSize: responsiveFontSize(1.8), + fontFamily: FontsFamily.nunitoBold, + marginBottom: 16, + }, + infoRow: { + flexDirection: 'row', + justifyContent: 'space-between', + paddingVertical: 10, + }, + infoLabel: { + fontSize: responsiveFontSize(1.6), + fontFamily: FontsFamily.nunitoMedium, + }, + infoValue: { + fontSize: responsiveFontSize(1.6), + fontFamily: FontsFamily.nunitoBold, + textTransform: 'capitalize', + }, + participantRow: { + flexDirection: 'row', + justifyContent: 'space-between', + alignItems: 'center', + paddingVertical: 12, + borderBottomWidth: 1, + }, + participantInfo: { + flexDirection: 'row', + alignItems: 'center', + gap: 12, + }, + avatar: { + width: 36, + height: 36, + borderRadius: 18, + justifyContent: 'center', alignItems: 'center', }, - joinButtonText: { - color: '#FFFFFF', - fontSize: responsiveFontSize(1.8), - fontWeight: 'bold', + avatarText: { + fontSize: 14, + fontFamily: FontsFamily.nunitoBold, }, - disabledButton: { - backgroundColor: '#9CA3AF', // Gray color when disabled + participantName: { + fontSize: responsiveFontSize(1.6), + fontFamily: FontsFamily.nunitoMedium, + }, + organizerBadge: { + paddingHorizontal: 10, + paddingVertical: 4, + borderRadius: 8, + }, + organizerBadgeText: { + fontSize: 10, + fontFamily: FontsFamily.nunitoBold, + textTransform: 'uppercase', + }, + emptyText: { + textAlign: 'center', + paddingVertical: 20, + fontSize: responsiveFontSize(1.6), + fontFamily: FontsFamily.nunitoMedium, + }, + joinButton: { + marginTop: 16, + }, + errorContainer: { + flex: 1, + justifyContent: 'center', + alignItems: 'center', + paddingHorizontal: 32, + }, + errorTitle: { + fontSize: 24, + fontFamily: FontsFamily.nunitoBold, + marginTop: 16, + }, + errorSubtitle: { + fontSize: 16, + fontFamily: FontsFamily.nunitoRegular, + textAlign: 'center', + marginTop: 8, + marginBottom: 32, }, }); export default InviteScreen; - diff --git a/src/screens/LoginScreen.tsx b/src/screens/LoginScreen.tsx index e0a618e..dbc8d98 100644 --- a/src/screens/LoginScreen.tsx +++ b/src/screens/LoginScreen.tsx @@ -1,328 +1,179 @@ -import React, { useState, useRef, useEffect } from 'react'; +import React, { useState, useEffect, useRef } from 'react'; import { View, Text, - TextInput, StyleSheet, - TouchableOpacity, + Animated, KeyboardAvoidingView, Platform, - Animated, - StatusBar, - ActivityIndicator, - Dimensions, } from 'react-native'; -import { LOGIN_URL } from '../utils/ApiUrls'; -import axios from 'axios'; import { useNavigation } from '@react-navigation/native'; -import type { StackNavigationProp } from '@react-navigation/stack'; +import { StackNavigationProp } from '@react-navigation/stack'; +import { responsiveFontSize } from 'react-native-responsive-dimensions'; import { RootStackParamList } from '../navigation/AppNavigator'; -import LinearGradient from 'react-native-linear-gradient'; -import Ionicons from '@react-native-vector-icons/ionicons'; -import MaterialRipple from 'react-native-material-ripple'; -import CustomAlert from '../components/CostomAlart'; -import colors from '../components/colors'; -import { - responsiveHeight, - responsiveWidth, - responsiveFontSize -} from "react-native-responsive-dimensions"; -import { useTheme } from '../theme/ThemeProvider'; +import { useTheme } from '../hooks/ui/useTheme'; +import { useFetch } from '../hooks/core/useFetch'; +import { LOGIN_URL } from '../utils/ApiUrls'; +import { CustomInput, CustomButton } from '../components/common/Custom'; +import AppView from '../components/AppView'; import FontsFamily from '../constents/FontsFamily'; - - -const { width } = Dimensions.get('window'); -const BLOCK_SIZE = width / 3; - - -// Note: You will need to install react-native-material-ripple -// npm install react-native-material-ripple - - export default function LoginScreen() { const navigation = useNavigation>(); + const theme = useTheme(); const [mobile, setMobile] = useState(''); - const [loading, setLoading] = useState(false); - const [alertInfo, setAlertInfo] = useState({ - visible: false, - title: '', - message: '', - buttons: [] as { text: string; onPress: () => void; style?: 'cancel' | 'destructive' | 'default' }[], - alertType: 'info' as 'info' | 'success' | 'error', - }); - - - const fade = useRef(new Animated.Value(0)).current; - const slide = useRef(new Animated.Value(30)).current; - const ctaScale = useRef(new Animated.Value(1)).current; - const { theme } = useTheme(); + const [error, setError] = useState(''); + const fadeAnim = useRef(new Animated.Value(0)).current; + const slideAnim = useRef(new Animated.Value(20)).current; useEffect(() => { Animated.parallel([ - Animated.timing(fade, { toValue: 1, duration: 800, useNativeDriver: true }), - Animated.spring(slide, { toValue: 0, friction: 5, tension: 40, useNativeDriver: true }), + Animated.timing(fadeAnim, { + toValue: 1, + duration: 800, + useNativeDriver: true, + }), + Animated.spring(slideAnim, { + toValue: 0, + tension: 20, + useNativeDriver: true, + }), ]).start(); - }, []); - - - const showAlert = ( - title: string, - message: string, - buttons: { text: string; onPress: () => void; style?: 'cancel' | 'destructive' | 'default' }[], - alertType: 'info' | 'success' | 'error' = 'info', - ) => { - setAlertInfo({ visible: true, title, message, buttons, alertType }); - }; - - - const hideAlert = () => { - setAlertInfo({ ...alertInfo, visible: false }); - }; + }, [fadeAnim, slideAnim]); + const { refetch: sendOtp, isFetching: loading } = useFetch( + LOGIN_URL, + { mobile }, + { + enabled: false, + retry: false, + } + ); const handleContinue = async () => { - if (mobile.length === 10) { - setLoading(true); - try { - const res = await axios.get(LOGIN_URL, { params: { mobile } }); - console.log("login res -",res) - if (res.data.statusCode === 200) { - navigation.navigate('OtpVarify', { mobile }); - } else { - showAlert('Login Failed', res.data.message || 'Could not send OTP.', [{ text: 'Try Again', onPress: hideAlert }], 'error'); - } - } catch (error: any) { - showAlert('An Error Occurred', 'Failed to send OTP. Please check your connection and try again.', [{ text: 'OK', onPress: hideAlert }], 'error'); - } finally { - setLoading(false); + if (!/^\d{10}$/.test(mobile)) { + setError('Please enter a 10-digit mobile number'); + return; + } + + setError(''); + try { + const { data } = await sendOtp(); + if (data?.statusCode === 200) { + navigation.navigate('OtpVarify', { mobile }); + } else { + setError(data?.message || 'Failed to send OTP. Please try again.'); } - } else { - showAlert('Invalid Number', 'Please enter a valid 10-digit mobile number.', [{ text: 'Got it', onPress: hideAlert }], 'info'); + } catch (err) { + setError('Something went wrong. Please check your connection.'); } }; - - const colorsBlocks = [ - theme.colors.logincard, - theme.colors.logincard1, - theme.colors.logincard1, - theme.colors.logincard, - theme.colors.logincard, - theme.colors.logincard1, - theme.colors.logincard1, - theme.colors.logincard, - theme.colors.logincard, - ]; - - return ( - - - - - {/* Funky grid blocks background (Unchanged) */} - - {colorsBlocks.map((bgColor, index) => ( - - - ))} - - - + - - - - CircleCollect - Welcome Back! - - Let's get you signed in to your community. - - + + + CircleCollect + Welcome back + + Enter your mobile number to sign in to your community. + + + { + setMobile(text.replace(/\D/g, '')); + if (error) setError(''); + }} + keyboardType="phone-pad" + maxLength={10} + icon="call-outline" + error={error} + /> - - - - - - - - - - - - {loading ? ( - - ) : ( - - Continue - - - )} - - - - Your privacy is our priority. Your information is always secure. + /> + + + + + Your information is secure and private. - - + +
- - - -
+ ); } - const styles = StyleSheet.create({ container: { flex: 1, }, - gridContainer: { - ...StyleSheet.absoluteFillObject, - flexDirection: 'row', - flexWrap: 'wrap', - }, - gridBlock: { - width: BLOCK_SIZE, - height: BLOCK_SIZE, - margin: 2, - borderRadius: 16, - }, - elevatedBlock: { - shadowColor: '#ffffffff', - shadowOffset: { width: 1, height: 5 }, - shadowOpacity: 1, - shadowRadius: 15, - elevation: 10, - }, - keyboardAvoidingContainer: { + keyboardView: { flex: 1, - }, - contentContainer: { - flex: 1, - justifyContent: 'space-between', - padding: 24, - }, - topSection: { - flex: 0.8, justifyContent: 'center', + }, + content: { + paddingHorizontal: 12, + }, + headerSection: { alignItems: 'center', + marginBottom: 48, }, brand: { - fontSize: responsiveFontSize(3), - color: colors.primary, - textAlign: 'center', - letterSpacing: 2, - marginBottom: 16, - textShadowColor: 'rgba(0, 0, 0, 0.1)', - textShadowOffset: { width: 1, height: 1 }, - textShadowRadius: 2, + fontSize: responsiveFontSize(2.2), fontFamily: FontsFamily.nunitoExtraBold, + letterSpacing: 1.5, + textTransform: 'uppercase', + marginBottom: 16, }, title: { - fontSize: responsiveFontSize(5), - fontFamily: FontsFamily.nunitoBold, - - textAlign: 'center', - marginBottom: 14, + fontSize: responsiveFontSize(3.5), + fontFamily: FontsFamily.nunitoBold, + marginBottom: 8, }, subtitle: { fontSize: responsiveFontSize(1.6), - fontFamily: FontsFamily.nunitoRegular, + fontFamily: FontsFamily.nunitoRegular, textAlign: 'center', - - lineHeight: 26, - maxWidth: 320, + paddingHorizontal: 24, + lineHeight: 24, }, - inputWrapper: { - flexDirection: 'row', - alignItems: 'center', - borderRadius: 36, - borderWidth: 2, - paddingHorizontal: responsiveHeight(2), - elevation: 8, - }, - inputIcon: { - marginRight: 16, - }, - input: { - flex: 1, - fontSize: responsiveFontSize(1.8), - fontFamily: FontsFamily.nunitoSemiBold, - color: colors.textLight, - height: responsiveHeight(5), - }, - bottomSection: { - alignItems: 'center', - paddingBottom: Platform.OS === 'ios' ? 20 : 10, + formSection: { + marginBottom: 32, }, button: { - width: '100%', - borderRadius: 30, - elevation: 14, - marginTop: 20, + marginTop: 16, }, - buttonGradient: { - - borderRadius:42, - paddingVertical: 14, - flexDirection: 'row', - justifyContent: 'center', + footerSection: { alignItems: 'center', }, - buttonContent: { - flexDirection: 'row', - alignItems: 'center', - }, - buttonText: { - color: colors.white, - fontSize: responsiveFontSize(1.8), - fontFamily: FontsFamily.nunitoBold, - }, footerText: { - fontSize: responsiveFontSize(1.4), - color: 'rgba(0,0,0,0.5)', - marginTop: 28, - textAlign: 'center', - maxWidth: 300, + fontSize: responsiveFontSize(1.3), fontFamily: FontsFamily.nunitoRegular, + textAlign: 'center', }, -}); \ No newline at end of file +}); diff --git a/src/screens/NotificationRedirection.tsx b/src/screens/NotificationRedirection.tsx index 8449698..6342f68 100644 --- a/src/screens/NotificationRedirection.tsx +++ b/src/screens/NotificationRedirection.tsx @@ -92,4 +92,4 @@ const styles = StyleSheet.create({ }, }); -export default NotificationRedirection; \ No newline at end of file +export default NotificationRedirection; diff --git a/src/screens/OrgnizerRagister.tsx b/src/screens/OrgnizerRagister.tsx index 8d9e02c..b71efe9 100644 --- a/src/screens/OrgnizerRagister.tsx +++ b/src/screens/OrgnizerRagister.tsx @@ -1,154 +1,181 @@ -import React, { useState } from "react"; +import React, { useState } from 'react'; import { - View, - Text, - TextInput, - Pressable, StyleSheet, - Alert, KeyboardAvoidingView, Platform, ScrollView, ToastAndroid, -} from "react-native"; -import axios from "axios"; -import { ORGRAGISTRATION } from "../utils/ApiUrls"; -import * as Keychain from "react-native-keychain"; -import { responsiveFontSize } from "react-native-responsive-dimensions"; + View, + Text, +} from 'react-native'; +import { useNavigation } from '@react-navigation/native'; +import { responsiveFontSize } from 'react-native-responsive-dimensions'; +import { useTheme } from '../hooks/ui/useTheme'; +import { useCreate } from '../hooks/core/useCreate'; +import { ORGRAGISTRATION } from '../utils/ApiUrls'; +import { CustomInput, CustomButton } from '../components/common/Custom'; +import FontsFamily from '../constents/FontsFamily'; +import AppView from '../components/AppView'; -const theme = { - primary: "#111", - accent: "#FF7A00", - background: "#F9F9F9", - text: "#1A1A1A", - subText: "#6B6B6B", - outline: "#E5E5E5", +type OrganizerForm = { + bankAccountNumber: string; + bankName: string; + branchCode: string; }; +type OrganizerFormErrors = Partial>; + +const isValidBankName = (value: string) => /^[A-Za-z][A-Za-z\s.&-]{1,}$/.test(value.trim()); +const isValidAccountNumber = (value: string) => /^\d{9,18}$/.test(value.trim()); +const isValidIfsc = (value: string) => /^[A-Z]{4}0[A-Z0-9]{6}$/.test(value.trim().toUpperCase()); + export default function OrganizerRegisterScreen() { - const [bankAccountNumber, setBankAccountNumber] = useState(""); - const [bankName, setBankName] = useState(""); - const [branchCode, setBranchCode] = useState(""); - const [loading, setLoading] = useState(false); -const handleSubmit = async () => { - if (!bankAccountNumber || !bankName || !branchCode) { - ToastAndroid.show('All fields are required.', ToastAndroid.SHORT); - return; - } + const theme = useTheme(); + const navigation = useNavigation(); - try { - setLoading(true); + const [form, setForm] = useState({ + bankAccountNumber: '', + bankName: '', + branchCode: '', + }); + const [errors, setErrors] = useState({}); - // 🔑 get token from Keychain - const creds = await Keychain.getGenericPassword(); - const token = creds ? creds.password : null; + const { mutate: register, isPending: isLoading } = useCreate(ORGRAGISTRATION); - if (!token) { - ToastAndroid.show('No token found. Please log in again.', ToastAndroid.SHORT); - setLoading(false); + const handleChange = (field: keyof OrganizerForm, value: string) => { + let nextValue = value; + if (field === 'bankAccountNumber') nextValue = value.replace(/\D/g, ''); + if (field === 'branchCode') nextValue = value.toUpperCase(); + + setForm(prev => ({ ...prev, [field]: nextValue })); + if (errors[field]) { + setErrors(prev => ({ ...prev, [field]: '' })); + } + }; + + const validateForm = () => { + const nextErrors: OrganizerFormErrors = {}; + + if (!isValidBankName(form.bankName)) { + nextErrors.bankName = 'Enter a valid bank name (min 2 letters).'; + } + if (!isValidAccountNumber(form.bankAccountNumber)) { + nextErrors.bankAccountNumber = 'Account number must be 9-18 digits.'; + } + if (!isValidIfsc(form.branchCode)) { + nextErrors.branchCode = 'Enter a valid IFSC (e.g. HDFC0001234).'; + } + + setErrors(nextErrors); + return Object.keys(nextErrors).length === 0; + }; + + const handleSubmit = () => { + if (!validateForm()) { + ToastAndroid.show('Please fix the highlighted fields.', ToastAndroid.SHORT); return; } - const payload = { - bankAccountNumber, - bankName, - branchCode, - }; - - const res = await axios.post( - ORGRAGISTRATION, // your API endpoint - payload, - { - headers: { - "Content-Type": "application/json", - Authorization: `Bearer ${token}`, // 👈 token added here - }, - } - ); - - console.log("✅ Organizer Registered:", res.data); - ToastAndroid.show('Organizer registered successfully!', ToastAndroid.SHORT); - // Optionally, reset form fields - setBankAccountNumber(""); - setBankName(""); - setBranchCode(""); - } catch (err: any) { - console.error("❌ Register Error:", err?.response?.data || err.message); - } finally { - setLoading(false); - } -}; - + register(form, { + onSuccess: () => { + ToastAndroid.show('You are now registered as an Organizer!', ToastAndroid.SHORT); + navigation.navigate('Home'); + }, + onError: (err) => { + ToastAndroid.show(err.message || 'Registration failed', ToastAndroid.SHORT); + }, + }); + }; return ( - - - Organizer Registration 📝 + + + + + Organizer Registration + + Provide your banking details to start managing your own circles. + + - + + handleChange('bankName', val)} + icon="business-outline" + error={errors.bankName} + /> - + handleChange('bankAccountNumber', val)} + keyboardType="numeric" + maxLength={18} + icon="card-outline" + error={errors.bankAccountNumber} + /> - + handleChange('branchCode', val)} + autoCapitalize="characters" + maxLength={11} + icon="code-working-outline" + error={errors.branchCode} + /> + - - {loading ? "Submitting..." : "Submit"} - - - + + + + ); } const styles = StyleSheet.create({ container: { flexGrow: 1, - justifyContent: "center", - padding: 20, + paddingBottom: 40, + }, + header: { + marginBottom: 32, + marginTop: 16, }, title: { - fontSize: responsiveFontSize(2.8), - fontWeight: "700", - color: theme.text, - marginBottom: 20, - textAlign: "center", + fontSize: responsiveFontSize(3.2), + fontFamily: FontsFamily.nunitoBold, }, - input: { + subtitle: { + fontSize: responsiveFontSize(1.8), + fontFamily: FontsFamily.nunitoRegular, + marginTop: 8, + lineHeight: 24, + }, + form: { + borderRadius: 20, borderWidth: 1, - borderColor: theme.outline, - borderRadius: 12, - padding: 14, - marginBottom: 15, - fontSize: responsiveFontSize(1.6), - backgroundColor: "#fff", + padding: 24, + marginBottom: 24, }, button: { - backgroundColor: theme.primary, - paddingVertical: 16, - borderRadius: 12, - alignItems: "center", - }, - buttonText: { - color: "#fff", - fontWeight: "600", - fontSize: responsiveFontSize(1.6), + marginTop: 'auto', }, }); diff --git a/src/screens/OtpVarificationScreen.tsx b/src/screens/OtpVarificationScreen.tsx index 3398f32..d8380ca 100644 --- a/src/screens/OtpVarificationScreen.tsx +++ b/src/screens/OtpVarificationScreen.tsx @@ -1,396 +1,254 @@ -import { CommonActions, RouteProp, useNavigation, useRoute } from "@react-navigation/native"; -import axios from "axios"; -import React, { useEffect, useRef, useState } from "react"; +import React, { useState, useEffect, useRef } from 'react'; import { View, Text, StyleSheet, TextInput, - Pressable, Animated, Platform, - NativeSyntheticEvent, - TextInputKeyPressEventData, KeyboardAvoidingView, - StatusBar, - Dimensions, -} from "react-native"; -import { RootStackParamList } from "../navigation/AppNavigator"; -import * as Keychain from "react-native-keychain"; -import { LOGIN_URL, VARIFY_OTP_URL } from "../utils/ApiUrls"; -import colors from "../components/colors"; -import CustomAlert from "../components/CostomAlart"; -import { getFcmToken } from "../utils/firebase/firebaseServices"; -import { useTheme } from "../theme/ThemeProvider"; -import FontsFamily from "../constents/FontsFamily"; + ToastAndroid, + TouchableOpacity, +} from 'react-native'; +import { useNavigation, useRoute, RouteProp, CommonActions } from '@react-navigation/native'; +import { responsiveFontSize } from 'react-native-responsive-dimensions'; +import * as Keychain from 'react-native-keychain'; +import { RootStackParamList } from '../navigation/AppNavigator'; +import { useTheme } from '../hooks/ui/useTheme'; +import { useCreate } from '../hooks/core/useCreate'; +import { LOGIN_URL, VARIFY_OTP_URL } from '../utils/ApiUrls'; +import { getFcmToken } from '../utils/firebase/firebaseServices'; +import { CustomButton } from '../components/common/Custom'; +import AppView from '../components/AppView'; +import FontsFamily from '../constents/FontsFamily'; const CELL_COUNT = 6; -const { width } = Dimensions.get('window'); -const BLOCK_SIZE = width / 3; -type OtpVerifyRouteProp = RouteProp; - -interface AlertButton { - text: string; - onPress: () => void; - style?: "cancel" | "destructive" | "default"; -} +type OtpVerifyRouteProp = RouteProp; export default function OtpVerificationScreen() { - const [code, setCode] = useState(Array(CELL_COUNT).fill("")); - const [timer, setTimer] = useState(30); - const [isResending, setIsResending] = useState(false); - const [alertConfig, setAlertConfig] = useState<{ - visible: boolean; - title: string; - message: string; - buttons: AlertButton[]; - alertType: 'error' | 'success' | 'info'; - }>({ - visible: false, - title: "", - message: "", - buttons: [], - alertType: "info", - }); - const { theme } = useTheme(); - - const inputs = useRef>([]); + const theme = useTheme(); + const navigation = useNavigation(); const route = useRoute(); const { mobile } = route.params; - const navigation = useNavigation(); - // Refs for auto-closing logic - const successActionRef = useRef<(() => void) | null>(null); - const successTimerRef = useRef(null); + const [code, setCode] = useState(Array(CELL_COUNT).fill('')); + const [timer, setTimer] = useState(30); + const inputs = useRef>([]); - const fade = useRef(new Animated.Value(0)).current; - const slide = useRef(new Animated.Value(30)).current; - const ctaScale = useRef(new Animated.Value(1)).current; + const fadeAnim = useRef(new Animated.Value(0)).current; + const slideAnim = useRef(new Animated.Value(20)).current; useEffect(() => { Animated.parallel([ - Animated.timing(fade, { toValue: 1, duration: 800, useNativeDriver: true }), - Animated.spring(slide, { toValue: 0, friction: 5, tension: 40, useNativeDriver: true }), + Animated.timing(fadeAnim, { + toValue: 1, + duration: 800, + useNativeDriver: true, + }), + Animated.spring(slideAnim, { + toValue: 0, + tension: 20, + useNativeDriver: true, + }), ]).start(); + + const interval = setInterval(() => { + setTimer((prev) => (prev > 0 ? prev - 1 : 0)); + }, 1000); + + return () => clearInterval(interval); }, []); - useEffect(() => { - if (timer <= 0) return; - const interval = setInterval(() => setTimer((prev) => prev - 1), 1000); - return () => clearInterval(interval); - }, [timer]); + const { mutate: verifyOtp, isPending: verifying } = useCreate(VARIFY_OTP_URL, { + onSuccess: async (res) => { + if (res?.statusCode === 200) { + const { jwt, userId } = res?.data || {}; + await Keychain.setGenericPassword('auth', jwt, { + accessible: Keychain.ACCESSIBLE.WHEN_UNLOCKED, + }); + await getFcmToken(userId); - const hideAlert = () => { - if (successTimerRef.current) { - clearTimeout(successTimerRef.current); - successTimerRef.current = null; - } - setAlertConfig(prev => ({ ...prev, visible: false })); - }; - - const triggerAlert = ( - title: string, - message: string, - buttons: AlertButton[], - alertType: 'error' | 'success' | 'info' = 'info', - autoCloseAction?: () => void - ) => { - setAlertConfig({ visible: true, title, message, buttons, alertType }); - - if (alertType === 'success' && autoCloseAction) { - successActionRef.current = autoCloseAction; - successTimerRef.current = setTimeout(() => { - successActionRef.current?.(); - }, 2000); - } - }; - - const saveToken = async (token: string) => { - try { - await Keychain.setGenericPassword("auth", token, { accessible: Keychain.ACCESSIBLE.WHEN_UNLOCKED }); - console.log("✅ Token saved securely"); - } catch (error) { - console.error("❌ Error saving token:", error); - } - }; - - const onSubmit = async (otp: string, mobile: string) => { - if (otp.length !== CELL_COUNT) { - triggerAlert( - 'Incomplete Code', - 'Please enter all 6 digits of the OTP.', - [{ text: 'OK', onPress: hideAlert }], - 'error' - ); - return; - } - try { - const res = await axios.post(VARIFY_OTP_URL, { mobile, otp }, { - headers: { "Content-Type": "application/json", Accept: "application/json" }, - }); - - if (res.data?.statusCode === 200) { - const { jwt } = res.data?.data || {}; - console.log("✅ OTP Verified:", res.data); - await saveToken(jwt); - await getFcmToken(res.data?.data?.userId); - - let hasActionRun = false; - const successAction = () => { - if (hasActionRun) return; - hasActionRun = true; - hideAlert(); - navigation.dispatch( - CommonActions.reset({ - index: 0, - routes: [{ name: 'Home' }], - }) - ); - }; - - triggerAlert( - 'Success!', - 'OTP verified successfully. Redirecting...', - [{ text: 'OK', onPress: successAction }], - 'success', - successAction + ToastAndroid.show('Verified successfully', ToastAndroid.SHORT); + navigation.dispatch( + CommonActions.reset({ + index: 0, + routes: [{ name: 'Home' }], + }) ); + } else { + ToastAndroid.show(res?.message || 'Invalid OTP', ToastAndroid.SHORT); } - } catch (err: any) { - console.error("❌ OTP Verification Error:", err?.response?.data || err.message); - triggerAlert( - 'Verification Failed', - err?.response?.data?.message || "Invalid OTP. Please try again.", - [{ text: 'Try Again', onPress: hideAlert, style: 'cancel' }], - 'error' - ); - } - }; + }, + onError: () => { + ToastAndroid.show('Verification failed. Please try again.', ToastAndroid.SHORT); + }, + }); - const resendOtp = async () => { - try { - setIsResending(true); + const { mutate: resendOtp, isPending: resending } = useCreate(LOGIN_URL, { + onSuccess: () => { setTimer(30); - await axios.get(LOGIN_URL, { params: { mobile } }) - - setCode(Array(CELL_COUNT).fill("")); + setCode(Array(CELL_COUNT).fill('')); inputs.current[0]?.focus(); + ToastAndroid.show('OTP Resent', ToastAndroid.SHORT); + }, + onError: () => { + ToastAndroid.show('Failed to resend OTP', ToastAndroid.SHORT); + }, + }); - let hasActionRun = false; - const successAction = () => { - if (hasActionRun) return; - hasActionRun = true; - hideAlert(); - }; + const handleTextChange = (text: string, index: number) => { + const newCode = [...code]; + newCode[index] = text.slice(-1); + setCode(newCode); - triggerAlert( - 'OTP Sent', - 'A new code has been sent to your mobile number.', - [{ text: 'OK', onPress: successAction }], - 'success', - successAction - ); - } catch (err: any) { - console.error("❌ Resend OTP Error:", err?.response?.data || err.message); - triggerAlert( - 'Error', - 'Failed to resend OTP. Try again later.', - [{ text: 'OK', onPress: hideAlert, style: 'cancel' }], - 'error' - ); - } finally { - setIsResending(false); + if (text && index < CELL_COUNT - 1) { + inputs.current[index + 1]?.focus(); + } + + if (newCode.every((cell) => cell !== '')) { + verifyOtp({ mobile, otp: newCode.join('') }); } }; - const submitIfComplete = (arr: string[]) => { - if (arr.join("").length === CELL_COUNT) { - onSubmit(arr.join(""), mobile); + const handleKeyPress = (e: any, index: number) => { + if (e.nativeEvent.key === 'Backspace' && !code[index] && index > 0) { + inputs.current[index - 1]?.focus(); } }; - const updateIndex = (idx: number, val: string) => { - const v = val.replace(/[^0-9]/g, ""); - const next = [...code]; - next[idx] = v.slice(-1); - setCode(next); - if (v && idx < CELL_COUNT - 1) inputs.current[idx + 1]?.focus(); - submitIfComplete(next); - }; - - const handleFirstCellChange = (val: string) => { - const digits = val.replace(/\D/g, ""); - if (digits.length <= 1) { - updateIndex(0, digits); - return; - } - const next = Array(CELL_COUNT).fill("").map((_, i) => digits[i] ?? ""); - setCode(next); - const firstEmpty = next.findIndex((c) => !c); - if (firstEmpty >= 0) { - inputs.current[firstEmpty]?.focus(); - } else { - inputs.current[CELL_COUNT - 1]?.blur(); - } - submitIfComplete(next); - }; - - const handleKeyPress = (idx: number, e: NativeSyntheticEvent) => { - if (e.nativeEvent.key === "Backspace" && !code[idx] && idx > 0) { - inputs.current[idx - 1]?.focus(); - } - }; - - const colorsBlocks = [ - theme.colors.logincard, - theme.colors.logincard1, - theme.colors.logincard1, - theme.colors.logincard, - theme.colors.logincard, - theme.colors.logincard1, - theme.colors.logincard1, - theme.colors.logincard, - theme.colors.logincard, - ]; - return ( - - - - - {colorsBlocks.map((bgColor, index) => ( - - ))} - - - - - - - - Verify Your Code - We’ve sent a 6-digit verification code to +91 {mobile}. - - - {Array.from({ length: CELL_COUNT }).map((_, i) => ( - { inputs.current[i] = r; }} - // UPDATED CELL STYLE - style={[ - styles.cell, - code[i] ? styles.cellFilled : null, - { - backgroundColor: theme.colors.optCard, - color: theme.colors.reverseText, - } - ]} - - keyboardType="number-pad" - maxLength={i === 0 ? CELL_COUNT : 1} - value={code[i]} - onChangeText={(t) => (i === 0 ? handleFirstCellChange(t) : updateIndex(i, t))} - onKeyPress={(e) => handleKeyPress(i, e)} - placeholder="*" - placeholderTextColor="#BDBDBD" - textContentType="oneTimeCode" - /> - ))} - + + + + + Verification + + Enter the 6-digit code sent to {'\n'} + +91 {mobile} + - - Animated.spring(ctaScale, { toValue: 0.97, useNativeDriver: true }).start()} - onPressOut={() => Animated.spring(ctaScale, { toValue: 1, useNativeDriver: true }).start()} - onPress={() => onSubmit(code.join(""), mobile)} - style={[styles.button, { backgroundColor: theme.colors.background }]} - > - Verify & Continue - + + {code.map((digit, index) => ( + { inputs.current[index] = ref }} + style={[ + styles.otpInput, + { + backgroundColor: theme.colors.surface, + color: theme.colors.textPrimary, + borderColor: digit ? theme.colors.primary : theme.colors.border, + }, + ]} + keyboardType="number-pad" + maxLength={1} + value={digit} + placeholder="0" + placeholderTextColor={theme.colors.textMuted} + onChangeText={(text) => handleTextChange(text, index)} + onKeyPress={(e) => handleKeyPress(e, index)} + selectionColor={theme.colors.primary} + /> + ))} + - {timer > 0 ? ( - Resend code in 00:{timer.toString().padStart(2, "0")} - ) : ( - - - {isResending ? "Resending..." : "Resend Code"} + + verifyOtp({ mobile, otp: code.join('') })} + loading={verifying} + style={styles.button} + /> + + + {timer > 0 ? ( + + Resend code in 00:{timer.toString().padStart(2, '0')} - - )} - - + ) : ( + resendOtp({ params: { mobile } })} disabled={resending}> + + {resending ? 'Sending...' : 'Resend Code'} + + + )} + + + - + ); } const styles = StyleSheet.create({ - container: { flex: 1 }, - gridContainer: { - ...StyleSheet.absoluteFillObject, + container: { + flex: 1, + }, + keyboardView: { + flex: 1, + justifyContent: 'center', + }, + content: { + paddingHorizontal: 12, + }, + headerSection: { + alignItems: 'center', + marginBottom: 48, + }, + title: { + fontSize: responsiveFontSize(3.5), + fontFamily: FontsFamily.nunitoBold, + marginBottom: 12, + }, + subtitle: { + fontSize: responsiveFontSize(1.6), + fontFamily: FontsFamily.nunitoRegular, + textAlign: 'center', + lineHeight: 24, + }, + otpSection: { flexDirection: 'row', - flexWrap: 'wrap', + justifyContent: 'space-between', + marginBottom: 40, }, - gridBlock: { - width: BLOCK_SIZE, - height: BLOCK_SIZE, - margin: 2, - borderRadius: 16, + otpInput: { + width: 48, + height: 56, + borderRadius: 12, + borderWidth: 1.5, + textAlign: 'center', + fontSize: responsiveFontSize(2.2), + fontFamily: FontsFamily.nunitoBold, }, - elevatedBlock: { - shadowColor: '#ffffffff', - shadowOffset: { width: 1, height: 5 }, - shadowOpacity: 1, - shadowRadius: 15, - elevation: 10, + actionSection: { + alignItems: 'center', }, - keyboardAvoidingContainer: { flex: 1, justifyContent: 'center' }, - contentPanel: { - marginHorizontal: 24, - borderRadius: 32, - padding: 28, - shadowColor: '#000', - shadowOffset: { width: 0, height: 10 }, - shadowOpacity: 0.14, - shadowRadius: 24, - elevation: 12, + button: { + width: '100%', + marginBottom: 24, }, - topSection: { alignItems: 'center', marginBottom: 24 }, - title: { fontSize: 28, color: colors.textDark, textAlign: 'center', marginBottom: 12 ,fontFamily: FontsFamily.nunitoExtraBold}, - subtitle: { fontSize: 16, color: colors.textLight, textAlign: 'center', marginBottom: 32, lineHeight: 24, maxWidth: 300 ,fontFamily: FontsFamily.nunitoMedium}, - - otpRow: { - flexDirection: "row", - justifyContent: "space-between", - width: '100%', - paddingHorizontal: 0, // Removed padding to use full width + resendContainer: { + alignItems: 'center', }, - - // FIX: Updated Cell Styling for clean smooth radius - cell: { - width: 45, - height: 52, - borderRadius: 14, // Smoother radius - textAlign: "center", - fontSize: 20, - fontWeight: "600", - elevation: 0, // Removed elevation for flat look (or keep if you want shadow) - overflow: 'hidden', + timerText: { + fontSize: responsiveFontSize(1.4), + fontFamily: FontsFamily.nunitoRegular, }, - cellFilled: { - // Optional: Add specific style when filled if needed, handled inline above + resendText: { + fontSize: responsiveFontSize(1.5), + fontFamily: FontsFamily.nunitoBold, + textDecorationLine: 'underline', }, - - bottomSection: { paddingBottom: Platform.OS === "ios" ? 20 : 10, paddingTop: 10 }, - button: { backgroundColor: colors.pastelPink, paddingVertical: 18, borderRadius: 30, alignItems: "center", justifyContent: "center",}, - buttonText: { color: colors.white, fontSize: 16, fontFamily: FontsFamily.nunitoBold }, - footerText: { textAlign: "center", fontFamily: FontsFamily.nunitoBold, fontSize: 14, marginTop: 20 }, }); diff --git a/src/screens/SplashScreen.tsx b/src/screens/SplashScreen.tsx index d41038b..5c84e78 100644 --- a/src/screens/SplashScreen.tsx +++ b/src/screens/SplashScreen.tsx @@ -1,133 +1,87 @@ -import React, { useEffect, useRef } from "react"; -import { View, Text, StyleSheet, ActivityIndicator, Animated, Alert, ToastAndroid } from "react-native"; -import * as Keychain from "react-native-keychain"; -import { useNavigation } from "@react-navigation/native"; -import { RootStackParamList } from "../navigation/AppNavigator"; -import { PROFILE_URL } from "../utils/ApiUrls"; -import { useDispatch } from "react-redux"; -import { clearUser, setUserData } from "../redux/userSlice"; -import colors from "../components/colors"; -import { StackNavigationProp } from "@react-navigation/stack"; +import React, { useEffect, useRef } from 'react'; +import { View, Text, StyleSheet, ActivityIndicator, Animated } from 'react-native'; +import { useNavigation } from '@react-navigation/native'; +import { StackNavigationProp } from '@react-navigation/stack'; +import { useDispatch } from 'react-redux'; +import { responsiveFontSize } from 'react-native-responsive-dimensions'; +import { RootStackParamList } from '../navigation/AppNavigator'; +import { PROFILE_URL } from '../utils/ApiUrls'; +import { setUserData, clearUser } from '../redux/userSlice'; +import { useTheme } from '../hooks/ui/useTheme'; +import { useFetch } from '../hooks/core/useFetch'; +import FontsFamily from '../constents/FontsFamily'; const SplashScreen = () => { const navigation = useNavigation>(); const dispatch = useDispatch(); - - - const fadeAnim = useRef(new Animated.Value(1)).current; + const theme = useTheme(); + const fadeAnim = useRef(new Animated.Value(0)).current; - const fetchUserProfile = async (dispatch: any, navigation: any) => { - try { - const credentials = await Keychain.getGenericPassword(); - if (!credentials) { - return null; - } - - const token = credentials.password; - const response = await fetch(PROFILE_URL, { - method: "GET", - headers: { - Accept: "*/*", - Authorization: `Bearer ${token}`, - }, - }); - - const result = await response.json(); - - if (response.status === 200 && result.status === "success") { - // Save user data in Redux - dispatch( - setUserData({ - id: result.data.id, - userId: result.data.userId, - username: result.data.username, - firstName: result.data.firstName, - lastName: result.data.lastName, - gender: result.data.gender, - mobile: result.data.mobile, - email: result.data.email, - address: result.data.address, - bankAccountNumber: result.data.bankAccountNumber, - bankName: result.data.bankName, - branchCode: result.data.branchCode, - profilePicture: result.data.filePath, - filePath: result.data.filePath, - imageBlob: result.data.imageBlob, - role: result.data.role, - organizerId: result.data.organizerId, - organizerStatus: result.data.organizerStatus, - profileStatus: true, - }) - ); - return result.data.role; - } else if (response.status === 401 || result.status === 401) { - // Unauthorized: reset credentials and navigate to login - await Keychain.resetGenericPassword(); - dispatch(clearUser()); - ToastAndroid.show(result.message || "Session expired. Please login again.", ToastAndroid.SHORT); - navigation.replace("Login"); - return null; - } else { - console.warn("Unexpected response:", result); - return null; - } - } catch (error) { - console.error("❌ Error fetching profile:", error); - ToastAndroid.show("Failed to fetch profile. Please try again.", ToastAndroid.SHORT); - return null; - } - }; + const { data, isError, isLoading, error } = useFetch(PROFILE_URL, {}, { + retry: false, + staleTime: 0, + }); useEffect(() => { - const startSplash = async () => { - const role = await fetchUserProfile(dispatch, navigation); + Animated.timing(fadeAnim, { + toValue: 1, + duration: 1000, + useNativeDriver: true, + }).start(); + }, []); - Animated.timing(fadeAnim, { - toValue: 0, - duration: 1500, - useNativeDriver: true, - delay: 1500, - }).start(() => { - // Navigate based on role - if (!role) { - navigation.replace("Login"); - } else if (role === "USER") { - navigation.replace("Home"); - } else if (role === "ORGANIZER") { - navigation.replace("Home"); // Replace with Organizer screen if you have one + useEffect(() => { + if (!isLoading) { + const timer = setTimeout(() => { + if (data && data.status === 'success') { + const userData = data.data; + dispatch( + setUserData({ + ...userData, + profilePicture: userData.filePath, + profileStatus: true, + }) + ); + navigation.replace('Home'); } else { - navigation.replace("Login"); + // If error is 401 or no data (not logged in) + dispatch(clearUser()); + navigation.replace('Login'); } - }); - }; + }, 1500); // Minimum splash time - startSplash(); - }, [navigation, fadeAnim, dispatch, ]); + return () => clearTimeout(timer); + } + }, [isLoading, data, isError]); return ( - - - Circle Collect - - + + + CircleCollect + Your Community Finance Hub + + ); }; -export default SplashScreen; - const styles = StyleSheet.create({ container: { flex: 1, - justifyContent: "center", - alignItems: "center", - backgroundColor: "#FFFFFF", + justifyContent: 'center', + alignItems: 'center', }, - title: { - fontSize: 28, - fontWeight: "800", - color: colors.Home, - letterSpacing: 1, + brand: { + fontSize: responsiveFontSize(4), + fontFamily: FontsFamily.nunitoExtraBold, + letterSpacing: 2, + }, + subtitle: { + fontSize: responsiveFontSize(1.6), + fontFamily: FontsFamily.nunitoRegular, + marginTop: 8, }, }); + +export default SplashScreen; diff --git a/src/screens/home/DeclineModal.tsx b/src/screens/home/DeclineModal.tsx index 93bd75e..12fafe4 100644 --- a/src/screens/home/DeclineModal.tsx +++ b/src/screens/home/DeclineModal.tsx @@ -1,74 +1,111 @@ -import React, { useState } from "react"; -import { View, Text, TextInput, Button, Modal, StyleSheet } from "react-native"; +import React, { useState } from 'react'; +import { View, Text, StyleSheet, Modal, TouchableWithoutFeedback, Keyboard } from 'react-native'; +import { responsiveFontSize } from 'react-native-responsive-dimensions'; +import { useTheme } from '../../hooks/ui/useTheme'; +import { CustomInput, CustomButton } from '../../components/common/Custom'; +import FontsFamily from '../../constents/FontsFamily'; -const DeclineModal = ({ visible, onClose, onSubmit }: { +const DeclineModal = ({ + visible, + onClose, + onSubmit, +}: { visible: boolean; onClose: () => void; onSubmit: (reason: string) => void; }) => { - const [reason, setReason] = useState(""); + const [reason, setReason] = useState(''); + const theme = useTheme(); + + const handleSubmit = () => { + if (reason.trim()) { + onSubmit(reason); + setReason(''); + } + }; return ( - - - - Decline Request - - -