diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 1ffbb19..5f8fbbb 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -2,12 +2,15 @@ name: ESLint on: push: - branches: [ main ] + branches: + - '**' pull_request: - branches: [ main ] - # Allow manual trigger from Actions tab workflow_dispatch: +permissions: + contents: read + security-events: write + jobs: lint: name: Run ESLint @@ -23,14 +26,18 @@ jobs: cache: 'npm' - name: Install dependencies - run: npm ci + run: | + npm ci + npm install -g expo-cli @expo/cli + + - name: Run Expo Lint + run: npx eslint . --max-warnings=50 - - name: Run ESLint + - name: Generate SARIF report run: npx eslint . --format @microsoft/eslint-formatter-sarif --output-file eslint-results.sarif - continue-on-error: true - - name: Upload ESLint analysis results + - name: Upload analysis results uses: github/codeql-action/upload-sarif@v3 with: sarif_file: eslint-results.sarif - wait-for-processing: true \ No newline at end of file + wait-for-processing: true diff --git a/.gitignore b/.gitignore index 4868bb7..ff1c5dd 100644 --- a/.gitignore +++ b/.gitignore @@ -37,9 +37,6 @@ dist/ web-build/ # Native -# Uncomment these if using CNG/Prebuild and want to ensure proper syncing -/ios -/android # Node node_modules/ diff --git a/android/app/build.gradle b/android/app/build.gradle index 0b356ae..30ae81b 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -4,27 +4,6 @@ apply plugin: "com.facebook.react" def projectRoot = rootDir.getAbsoluteFile().getParentFile().getAbsolutePath() -static def versionToNumber(major, minor, patch) { - return patch * 100 + minor * 10000 + major * 1000000 -} - -def getRNVersion() { - def version = providers.exec { - workingDir(projectDir) - commandLine("node", "-e", "console.log(require('react-native/package.json').version);") - }.standardOutput.asText.get().trim() - - def coreVersion = version.split("-")[0] - def (major, minor, patch) = coreVersion.tokenize('.').collect { it.toInteger() } - - return versionToNumber( - major, - minor, - patch - ) -} -def rnVersion = getRNVersion() - /** * This is the configuration block to customize your React Native Android app. * By default you don't need to apply any configuration, just uncomment the lines you need. @@ -41,12 +20,12 @@ react { bundleCommand = "export:embed" /* Folders */ - // The root of your project, i.e. where "package.json" lives. Default is '..' - // root = file("../") - // The folder where the react-native NPM package is. Default is ../node_modules/react-native - // reactNativeDir = file("../node_modules/react-native") - // The folder where the react-native Codegen package is. Default is ../node_modules/@react-native/codegen - // codegenDir = file("../node_modules/@react-native/codegen") + // The root of your project, i.e. where "package.json" lives. Default is '../..' + // root = file("../../") + // The folder where the react-native NPM package is. Default is ../../node_modules/react-native + // reactNativeDir = file("../../node_modules/react-native") + // The folder where the react-native Codegen package is. Default is ../../node_modules/@react-native/codegen + // codegenDir = file("../../node_modules/@react-native/codegen") /* Variants */ // The list of variants to that are debuggable. For those we're going to @@ -79,10 +58,8 @@ react { // The list of flags to pass to the Hermes compiler. By default is "-O", "-output-source-map" // hermesFlags = ["-O", "-output-source-map"] - if (rnVersion >= versionToNumber(0, 75, 0)) { - /* Autolinking */ - autolinkLibrariesWithApp() - } + /* Autolinking */ + autolinkLibrariesWithApp() } /** @@ -114,8 +91,8 @@ android { applicationId 'me.tripsit.mobile' minSdkVersion rootProject.ext.minSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion - versionCode 23 - versionName "12.0.3" + versionCode 25 + versionName "12.0.4" } signingConfigs { debug { @@ -144,6 +121,9 @@ android { useLegacyPackaging (findProperty('expo.useLegacyPackaging')?.toBoolean() ?: false) } } + androidResources { + ignoreAssetsPattern '!.svn:!.git:!.ds_store:!*.scc:!CVS:!thumbs.db:!picasa.ini:!*~' + } } // Apply static values from `gradle.properties` to the `android.packagingOptions` @@ -194,8 +174,3 @@ dependencies { implementation jscFlavor } } - -if (rnVersion < versionToNumber(0, 75, 0)) { - apply from: new File(["node", "--print", "require.resolve('@react-native-community/cli-platform-android/package.json', { paths: [require.resolve('react-native/package.json')] })"].execute(null, rootDir).text.trim(), "../native_modules.gradle"); - applyNativeModulesAppBuildGradle(project) -} diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index 4c2ec2e..5d4a630 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -11,7 +11,7 @@ - + @@ -30,6 +30,5 @@ - \ No newline at end of file diff --git a/android/app/src/main/java/me/tripsit/mobile/MainActivity.kt b/android/app/src/main/java/me/tripsit/mobile/MainActivity.kt index 3daf717..2c550e3 100644 --- a/android/app/src/main/java/me/tripsit/mobile/MainActivity.kt +++ b/android/app/src/main/java/me/tripsit/mobile/MainActivity.kt @@ -1,5 +1,5 @@ -import expo.modules.splashscreen.SplashScreenManager package me.tripsit.mobile +import expo.modules.splashscreen.SplashScreenManager import android.os.Build import android.os.Bundle diff --git a/android/app/src/main/java/me/tripsit/mobile/MainApplication.kt b/android/app/src/main/java/me/tripsit/mobile/MainApplication.kt index aac4375..981d21d 100644 --- a/android/app/src/main/java/me/tripsit/mobile/MainApplication.kt +++ b/android/app/src/main/java/me/tripsit/mobile/MainApplication.kt @@ -10,6 +10,7 @@ import com.facebook.react.ReactPackage import com.facebook.react.ReactHost import com.facebook.react.defaults.DefaultNewArchitectureEntryPoint.load import com.facebook.react.defaults.DefaultReactNativeHost +import com.facebook.react.soloader.OpenSourceMergedSoMapping import com.facebook.soloader.SoLoader import expo.modules.ApplicationLifecycleDispatcher @@ -21,9 +22,10 @@ class MainApplication : Application(), ReactApplication { this, object : DefaultReactNativeHost(this) { override fun getPackages(): List { + val packages = PackageList(this).packages // Packages that cannot be autolinked yet can be added manually here, for example: // packages.add(new MyReactNativePackage()); - return PackageList(this).packages + return packages } override fun getJSMainModuleName(): String = ".expo/.virtual-metro-entry" @@ -40,7 +42,7 @@ class MainApplication : Application(), ReactApplication { override fun onCreate() { super.onCreate() - SoLoader.init(this, false) + SoLoader.init(this, OpenSourceMergedSoMapping) if (BuildConfig.IS_NEW_ARCHITECTURE_ENABLED) { // If you opted-in for the New Architecture, we load the native entry point for this app. load() diff --git a/android/app/src/main/res/drawable-hdpi/splashscreen_image.png b/android/app/src/main/res/drawable-hdpi/splashscreen_image.png deleted file mode 100644 index 0ba1f52..0000000 Binary files a/android/app/src/main/res/drawable-hdpi/splashscreen_image.png and /dev/null differ diff --git a/android/app/src/main/res/drawable-hdpi/splashscreen_logo.png b/android/app/src/main/res/drawable-hdpi/splashscreen_logo.png new file mode 100644 index 0000000..24938ee Binary files /dev/null and b/android/app/src/main/res/drawable-hdpi/splashscreen_logo.png differ diff --git a/android/app/src/main/res/drawable-mdpi/splashscreen_image.png b/android/app/src/main/res/drawable-mdpi/splashscreen_image.png deleted file mode 100644 index 0ba1f52..0000000 Binary files a/android/app/src/main/res/drawable-mdpi/splashscreen_image.png and /dev/null differ diff --git a/android/app/src/main/res/drawable-mdpi/splashscreen_logo.png b/android/app/src/main/res/drawable-mdpi/splashscreen_logo.png new file mode 100644 index 0000000..86fc087 Binary files /dev/null and b/android/app/src/main/res/drawable-mdpi/splashscreen_logo.png differ diff --git a/android/app/src/main/res/drawable-xhdpi/splashscreen_image.png b/android/app/src/main/res/drawable-xhdpi/splashscreen_image.png deleted file mode 100644 index 0ba1f52..0000000 Binary files a/android/app/src/main/res/drawable-xhdpi/splashscreen_image.png and /dev/null differ diff --git a/android/app/src/main/res/drawable-xhdpi/splashscreen_logo.png b/android/app/src/main/res/drawable-xhdpi/splashscreen_logo.png new file mode 100644 index 0000000..541140b Binary files /dev/null and b/android/app/src/main/res/drawable-xhdpi/splashscreen_logo.png differ diff --git a/android/app/src/main/res/drawable-xxhdpi/splashscreen_image.png b/android/app/src/main/res/drawable-xxhdpi/splashscreen_image.png deleted file mode 100644 index 0ba1f52..0000000 Binary files a/android/app/src/main/res/drawable-xxhdpi/splashscreen_image.png and /dev/null differ diff --git a/android/app/src/main/res/drawable-xxhdpi/splashscreen_logo.png b/android/app/src/main/res/drawable-xxhdpi/splashscreen_logo.png new file mode 100644 index 0000000..3cfb8c7 Binary files /dev/null and b/android/app/src/main/res/drawable-xxhdpi/splashscreen_logo.png differ diff --git a/android/app/src/main/res/drawable-xxxhdpi/splashscreen_image.png b/android/app/src/main/res/drawable-xxxhdpi/splashscreen_image.png deleted file mode 100644 index 0ba1f52..0000000 Binary files a/android/app/src/main/res/drawable-xxxhdpi/splashscreen_image.png and /dev/null differ diff --git a/android/app/src/main/res/drawable-xxxhdpi/splashscreen_logo.png b/android/app/src/main/res/drawable-xxxhdpi/splashscreen_logo.png new file mode 100644 index 0000000..69b1fa2 Binary files /dev/null and b/android/app/src/main/res/drawable-xxxhdpi/splashscreen_logo.png differ diff --git a/android/app/src/main/res/drawable/splashscreen.xml b/android/app/src/main/res/drawable/ic_launcher_background.xml similarity index 59% rename from android/app/src/main/res/drawable/splashscreen.xml rename to android/app/src/main/res/drawable/ic_launcher_background.xml index c8568e1..883b2a0 100644 --- a/android/app/src/main/res/drawable/splashscreen.xml +++ b/android/app/src/main/res/drawable/ic_launcher_background.xml @@ -1,3 +1,6 @@ + + + \ No newline at end of file diff --git a/android/app/src/main/res/mipmap-hdpi/ic_launcher.webp b/android/app/src/main/res/mipmap-hdpi/ic_launcher.webp new file mode 100644 index 0000000..fb2a0b2 Binary files /dev/null and b/android/app/src/main/res/mipmap-hdpi/ic_launcher.webp differ diff --git a/android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png b/android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png deleted file mode 100644 index 5fbc732..0000000 Binary files a/android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png and /dev/null differ diff --git a/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.webp similarity index 100% rename from android/app/src/main/res/mipmap-hdpi/ic_launcher.png rename to android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.webp diff --git a/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png b/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png deleted file mode 100644 index 7b10308..0000000 Binary files a/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png and /dev/null differ diff --git a/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp b/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp new file mode 100644 index 0000000..182a9d0 Binary files /dev/null and b/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp differ diff --git a/android/app/src/main/res/mipmap-mdpi/ic_launcher.webp b/android/app/src/main/res/mipmap-mdpi/ic_launcher.webp new file mode 100644 index 0000000..3add6de Binary files /dev/null and b/android/app/src/main/res/mipmap-mdpi/ic_launcher.webp differ diff --git a/android/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png b/android/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png deleted file mode 100644 index 97e77f7..0000000 Binary files a/android/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png and /dev/null differ diff --git a/android/app/src/main/res/mipmap-mdpi/ic_launcher.png b/android/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.webp similarity index 100% rename from android/app/src/main/res/mipmap-mdpi/ic_launcher.png rename to android/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.webp diff --git a/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png b/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png deleted file mode 100644 index 9bc29ea..0000000 Binary files a/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png and /dev/null differ diff --git a/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp b/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp new file mode 100644 index 0000000..01dcc96 Binary files /dev/null and b/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp differ diff --git a/android/app/src/main/res/mipmap-xhdpi/ic_launcher.webp b/android/app/src/main/res/mipmap-xhdpi/ic_launcher.webp new file mode 100644 index 0000000..a8fdfee Binary files /dev/null and b/android/app/src/main/res/mipmap-xhdpi/ic_launcher.webp differ diff --git a/android/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png b/android/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png deleted file mode 100644 index 69545ad..0000000 Binary files a/android/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png and /dev/null differ diff --git a/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/android/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.webp similarity index 100% rename from android/app/src/main/res/mipmap-xhdpi/ic_launcher.png rename to android/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.webp diff --git a/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png b/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png deleted file mode 100644 index 3d01962..0000000 Binary files a/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png and /dev/null differ diff --git a/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp b/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp new file mode 100644 index 0000000..d07f364 Binary files /dev/null and b/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp differ diff --git a/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp new file mode 100644 index 0000000..987c9bc Binary files /dev/null and b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp differ diff --git a/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png deleted file mode 100644 index f518fe5..0000000 Binary files a/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png and /dev/null differ diff --git a/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.webp similarity index 100% rename from android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png rename to android/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.webp diff --git a/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png deleted file mode 100644 index f491e9c..0000000 Binary files a/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png and /dev/null differ diff --git a/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp new file mode 100644 index 0000000..02db488 Binary files /dev/null and b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp differ diff --git a/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp new file mode 100644 index 0000000..5b52d0b Binary files /dev/null and b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp differ diff --git a/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png deleted file mode 100644 index b5bf734..0000000 Binary files a/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png and /dev/null differ diff --git a/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.webp similarity index 100% rename from android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png rename to android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.webp diff --git a/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png deleted file mode 100644 index a56ed79..0000000 Binary files a/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png and /dev/null differ diff --git a/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp new file mode 100644 index 0000000..d7732bd Binary files /dev/null and b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp differ diff --git a/android/app/src/main/res/values/strings.xml b/android/app/src/main/res/values/strings.xml index 44d386d..d0ea526 100644 --- a/android/app/src/main/res/values/strings.xml +++ b/android/app/src/main/res/values/strings.xml @@ -3,5 +3,5 @@ contain false automatic - 12.0.3 + 12.0.4 \ No newline at end of file diff --git a/android/build.gradle b/android/build.gradle index 932bf7b..abbcb8e 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -2,11 +2,11 @@ buildscript { ext { - buildToolsVersion = findProperty('android.buildToolsVersion') ?: '34.0.0' - minSdkVersion = Integer.parseInt(findProperty('android.minSdkVersion') ?: '23') - compileSdkVersion = Integer.parseInt(findProperty('android.compileSdkVersion') ?: '34') + buildToolsVersion = findProperty('android.buildToolsVersion') ?: '35.0.0' + minSdkVersion = Integer.parseInt(findProperty('android.minSdkVersion') ?: '24') + compileSdkVersion = Integer.parseInt(findProperty('android.compileSdkVersion') ?: '35') targetSdkVersion = Integer.parseInt(findProperty('android.targetSdkVersion') ?: '34') - kotlinVersion = findProperty('android.kotlinVersion') ?: '1.9.23' + kotlinVersion = findProperty('android.kotlinVersion') ?: '1.9.25' ndkVersion = "26.1.10909125" } diff --git a/android/gradle.properties b/android/gradle.properties index 40220de..7531e9e 100644 --- a/android/gradle.properties +++ b/android/gradle.properties @@ -22,9 +22,6 @@ org.gradle.jvmargs=-Xmx2048m -XX:MaxMetaspaceSize=512m # https://developer.android.com/topic/libraries/support-library/androidx-rn android.useAndroidX=true -# Automatically convert third-party libraries to use AndroidX -android.enableJetifier=true - # Enable AAPT2 PNG crunching android.enablePngCrunchInReleaseBuilds=true @@ -38,7 +35,7 @@ reactNativeArchitectures=armeabi-v7a,arm64-v8a,x86,x86_64 # your application. You should enable this flag either if you want # to write custom TurboModules/Fabric components OR use libraries that # are providing them. -newArchEnabled=false +newArchEnabled=true # Use this property to enable or disable the Hermes JS engine. # If set to false, you will be using JSC instead. diff --git a/android/gradle/wrapper/gradle-wrapper.jar b/android/gradle/wrapper/gradle-wrapper.jar index e644113..a4b76b9 100644 Binary files a/android/gradle/wrapper/gradle-wrapper.jar and b/android/gradle/wrapper/gradle-wrapper.jar differ diff --git a/android/gradle/wrapper/gradle-wrapper.properties b/android/gradle/wrapper/gradle-wrapper.properties index 6f7a6eb..79eb9d0 100644 --- a/android/gradle/wrapper/gradle-wrapper.properties +++ b/android/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.8-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.10.2-all.zip networkTimeout=10000 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME diff --git a/android/gradlew b/android/gradlew old mode 100755 new mode 100644 index 1aa94a4..f5feea6 --- a/android/gradlew +++ b/android/gradlew @@ -15,6 +15,8 @@ # See the License for the specific language governing permissions and # limitations under the License. # +# SPDX-License-Identifier: Apache-2.0 +# ############################################################################## # @@ -55,7 +57,7 @@ # Darwin, MinGW, and NonStop. # # (3) This script is generated from the Groovy template -# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt # within the Gradle project. # # You can find Gradle at https://github.com/gradle/gradle/. @@ -84,7 +86,8 @@ done # shellcheck disable=SC2034 APP_BASE_NAME=${0##*/} # Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) -APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit +APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s +' "$PWD" ) || exit # Use the maximum available, or set MAX_FD != -1 to use that value. MAX_FD=maximum diff --git a/android/gradlew.bat b/android/gradlew.bat index 7101f8e..9d21a21 100644 --- a/android/gradlew.bat +++ b/android/gradlew.bat @@ -1,92 +1,94 @@ -@rem -@rem Copyright 2015 the original author or authors. -@rem -@rem Licensed under the Apache License, Version 2.0 (the "License"); -@rem you may not use this file except in compliance with the License. -@rem You may obtain a copy of the License at -@rem -@rem https://www.apache.org/licenses/LICENSE-2.0 -@rem -@rem Unless required by applicable law or agreed to in writing, software -@rem distributed under the License is distributed on an "AS IS" BASIS, -@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -@rem See the License for the specific language governing permissions and -@rem limitations under the License. -@rem - -@if "%DEBUG%"=="" @echo off -@rem ########################################################################## -@rem -@rem Gradle startup script for Windows -@rem -@rem ########################################################################## - -@rem Set local scope for the variables with windows NT shell -if "%OS%"=="Windows_NT" setlocal - -set DIRNAME=%~dp0 -if "%DIRNAME%"=="" set DIRNAME=. -@rem This is normally unused -set APP_BASE_NAME=%~n0 -set APP_HOME=%DIRNAME% - -@rem Resolve any "." and ".." in APP_HOME to make it shorter. -for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi - -@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" - -@rem Find java.exe -if defined JAVA_HOME goto findJavaFromJavaHome - -set JAVA_EXE=java.exe -%JAVA_EXE% -version >NUL 2>&1 -if %ERRORLEVEL% equ 0 goto execute - -echo. 1>&2 -echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 -echo. 1>&2 -echo Please set the JAVA_HOME variable in your environment to match the 1>&2 -echo location of your Java installation. 1>&2 - -goto fail - -:findJavaFromJavaHome -set JAVA_HOME=%JAVA_HOME:"=% -set JAVA_EXE=%JAVA_HOME%/bin/java.exe - -if exist "%JAVA_EXE%" goto execute - -echo. 1>&2 -echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 -echo. 1>&2 -echo Please set the JAVA_HOME variable in your environment to match the 1>&2 -echo location of your Java installation. 1>&2 - -goto fail - -:execute -@rem Setup the command line - -set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar - - -@rem Execute Gradle -"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* - -:end -@rem End local scope for the variables with windows NT shell -if %ERRORLEVEL% equ 0 goto mainEnd - -:fail -rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of -rem the _cmd.exe /c_ return code! -set EXIT_CODE=%ERRORLEVEL% -if %EXIT_CODE% equ 0 set EXIT_CODE=1 -if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% -exit /b %EXIT_CODE% - -:mainEnd -if "%OS%"=="Windows_NT" endlocal - -:omega +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem +@rem SPDX-License-Identifier: Apache-2.0 +@rem + +@if "%DEBUG%"=="" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%"=="" set DIRNAME=. +@rem This is normally unused +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if %ERRORLEVEL% equ 0 goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* + +:end +@rem End local scope for the variables with windows NT shell +if %ERRORLEVEL% equ 0 goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +set EXIT_CODE=%ERRORLEVEL% +if %EXIT_CODE% equ 0 set EXIT_CODE=1 +if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% +exit /b %EXIT_CODE% + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/android/react-settings-plugin/build.gradle.kts b/android/react-settings-plugin/build.gradle.kts deleted file mode 100644 index b4f6668..0000000 --- a/android/react-settings-plugin/build.gradle.kts +++ /dev/null @@ -1,19 +0,0 @@ -import org.jetbrains.kotlin.gradle.tasks.KotlinCompile - -plugins { - kotlin("jvm") version "1.9.24" - id("java-gradle-plugin") -} - -repositories { - mavenCentral() -} - -gradlePlugin { - plugins { - create("reactSettingsPlugin") { - id = "com.facebook.react.settings" - implementationClass = "expo.plugins.ReactSettingsPlugin" - } - } -} diff --git a/android/react-settings-plugin/src/main/kotlin/expo/plugins/ReactSettingsPlugin.kt b/android/react-settings-plugin/src/main/kotlin/expo/plugins/ReactSettingsPlugin.kt deleted file mode 100644 index c54f6c7..0000000 --- a/android/react-settings-plugin/src/main/kotlin/expo/plugins/ReactSettingsPlugin.kt +++ /dev/null @@ -1,10 +0,0 @@ -package expo.plugins - -import org.gradle.api.Plugin -import org.gradle.api.initialization.Settings - -class ReactSettingsPlugin : Plugin { - override fun apply(settings: Settings) { - // Do nothing, just register the plugin. - } -} diff --git a/android/settings.gradle b/android/settings.gradle index a6d952c..83d4a5f 100644 --- a/android/settings.gradle +++ b/android/settings.gradle @@ -1,46 +1,23 @@ pluginManagement { - def version = providers.exec { - commandLine("node", "-e", "console.log(require('react-native/package.json').version);") - }.standardOutput.asText.get().trim() - def (_, reactNativeMinor, reactNativePatch) = version.split("-")[0].tokenize('.').collect { it.toInteger() } - - includeBuild(new File(["node", "--print", "require.resolve('@react-native/gradle-plugin/package.json')"].execute(null, rootDir).text.trim()).getParentFile().toString()) - if(reactNativeMinor == 74 && reactNativePatch <= 3){ - includeBuild("react-settings-plugin") - } + includeBuild(new File(["node", "--print", "require.resolve('@react-native/gradle-plugin/package.json', { paths: [require.resolve('react-native/package.json')] })"].execute(null, rootDir).text.trim()).getParentFile().toString()) } - plugins { id("com.facebook.react.settings") } -def getRNMinorVersion() { - def version = providers.exec { - commandLine("node", "-e", "console.log(require('react-native/package.json').version);") - }.standardOutput.asText.get().trim() - - def coreVersion = version.split("-")[0] - def (major, minor, patch) = coreVersion.tokenize('.').collect { it.toInteger() } - - return minor -} - -if (getRNMinorVersion() >= 75) { - extensions.configure(com.facebook.react.ReactSettingsExtension) { ex -> - if (System.getenv('EXPO_UNSTABLE_CORE_AUTOLINKING') == '1') { - println('\u001B[32mUsing expo-modules-autolinking as core autolinking source\u001B[0m') - def command = [ - 'node', - '--no-warnings', - '--eval', - 'require(require.resolve(\'expo-modules-autolinking\', { paths: [require.resolve(\'expo/package.json\')] }))(process.argv.slice(1))', - 'react-native-config', - '--json', - '--platform', - 'android' - ].toList() - ex.autolinkLibrariesFromCommand(command) - } else { - ex.autolinkLibrariesFromCommand() - } +extensions.configure(com.facebook.react.ReactSettingsExtension) { ex -> + if (System.getenv('EXPO_USE_COMMUNITY_AUTOLINKING') == '1') { + ex.autolinkLibrariesFromCommand() + } else { + def command = [ + 'node', + '--no-warnings', + '--eval', + 'require(require.resolve(\'expo-modules-autolinking\', { paths: [require.resolve(\'expo/package.json\')] }))(process.argv.slice(1))', + 'react-native-config', + '--json', + '--platform', + 'android' + ].toList() + ex.autolinkLibrariesFromCommand(command) } } @@ -57,10 +34,5 @@ dependencyResolutionManagement { apply from: new File(["node", "--print", "require.resolve('expo/package.json')"].execute(null, rootDir).text.trim(), "../scripts/autolinking.gradle"); useExpoModules() -if (getRNMinorVersion() < 75) { - apply from: new File(["node", "--print", "require.resolve('@react-native-community/cli-platform-android/package.json', { paths: [require.resolve('react-native/package.json')] })"].execute(null, rootDir).text.trim(), "../native_modules.gradle"); - applyNativeModulesSettingsGradle(settings) -} - include ':app' includeBuild(new File(["node", "--print", "require.resolve('@react-native/gradle-plugin/package.json', { paths: [require.resolve('react-native/package.json')] })"].execute(null, rootDir).text.trim()).getParentFile()) diff --git a/app.json b/app.json index e37d75a..bed05c0 100644 --- a/app.json +++ b/app.json @@ -2,7 +2,7 @@ "expo": { "name": "TripSit", "slug": "TripSit", - "version": "12.0.3", + "version": "12.0.4", "orientation": "portrait", "icon": "./assets/images/icon.png", "scheme": "myapp", @@ -20,13 +20,13 @@ "bundleIdentifier": "me.tripsit.mobile" }, "android": { - "versionCode": 23, + "versionCode": 25, "adaptiveIcon": { "foregroundImage": "./assets/images/adaptive-icon.png", "backgroundColor": "#ffffff" }, "package": "me.tripsit.mobile", - "runtimeVersion": "12.0.3" + "runtimeVersion": "12.0.4" }, "web": { "bundler": "metro", @@ -35,7 +35,8 @@ }, "plugins": [ "expo-router", - "expo-font" + "expo-font", + "expo-localization" ], "experiments": { "typedRoutes": true diff --git a/app/components/DrugDetail.tsx b/app/components/DrugDetail.tsx index f6cff42..3879e91 100644 --- a/app/components/DrugDetail.tsx +++ b/app/components/DrugDetail.tsx @@ -1,4 +1,4 @@ -import React, { useEffect } from 'react'; +import React, { useEffect, useState } from 'react'; import { View, StyleSheet, @@ -6,7 +6,8 @@ import { Dimensions, Linking, ScrollView, - TouchableOpacity, + TouchableOpacity, + useColorScheme, } from 'react-native'; import { Chip, @@ -24,6 +25,10 @@ import { Tooltip, Avatar, AnimatedFAB, + SegmentedButtons, + IconButton, + Badge, + ProgressBar, } from 'react-native-paper'; import { LineChart } from 'react-native-chart-kit'; import { MaterialCommunityIcons } from '@expo/vector-icons'; @@ -36,7 +41,12 @@ import Animated, { runOnJS, FadeIn, SlideInRight, + interpolate, + Extrapolate, + useAnimatedScrollHandler, } from 'react-native-reanimated'; +import AsyncStorage from '@react-native-async-storage/async-storage'; +import { format, formatDistanceToNow } from 'date-fns'; const screenWidth = Dimensions.get('window').width; @@ -89,20 +99,53 @@ type DrugDetailScreenProps = { }; const AnimatedSurface = Animated.createAnimatedComponent(Surface); +const AnimatedCard = Animated.createAnimatedComponent(Card); +const AnimatedScrollView = Animated.createAnimatedComponent(ScrollView); const DrugDetailScreen: React.FC = ({ drug, onClose }) => { const theme = useTheme(); + const isDarkMode = useColorScheme() === 'dark'; + const [selectedTab, setSelectedTab] = useState('overview'); + const [lastUpdated, setLastUpdated] = useState(null); + const windowWidth = Dimensions.get('window').width; const headerOpacity = useSharedValue(0); const contentOpacity = useSharedValue(0); const slideIn = useSharedValue(-100); + const scrollY = useSharedValue(0); const drugDetails = drug.details; - const headerAnimationStyle = useAnimatedStyle(() => ({ - opacity: headerOpacity.value, - transform: [{ translateY: slideIn.value }] - })); + useEffect(() => { + const getLastUpdated = async () => { + try { + const timestamp = await AsyncStorage.getItem('drugDataLastUpdated'); + if (timestamp) { + setLastUpdated(formatDistanceToNow(new Date(timestamp), { addSuffix: true })); + } + } catch (error) { + console.error('Error getting last updated timestamp:', error); + } + }; + + getLastUpdated(); + }, []); + + const headerAnimationStyle = useAnimatedStyle(() => { + const elevation = interpolate( + scrollY.value, + [0, 50], + [0, 4], + Extrapolate.CLAMP + ); + + return { + opacity: headerOpacity.value, + transform: [{ translateY: slideIn.value }], + elevation, + shadowOpacity: elevation / 4, + }; + }); const contentAnimationStyle = useAnimatedStyle(() => ({ opacity: contentOpacity.value, @@ -210,14 +253,264 @@ const DrugDetailScreen: React.FC = ({ drug, onClose }) => datasets: [ { data: intensities, - color: (opacity = 1) => theme.colors.primary + (opacity !== 1 ? opacity * 255 : ''), + color: (opacity = 1) => + theme.colors.primary + (opacity !== 1 ? Math.round(opacity * 255).toString(16) : ''), strokeWidth: 2, }, ], }; }; - const chartData = generateChartData(); + const generateTimelineData = () => { + if (!drugDetails) return null; + + const onset = drugDetails.formatted_onset?.value; + const duration = drugDetails.formatted_duration?.value; + const aftereffects = drugDetails.formatted_aftereffects?.value; + + const onsetMinutes = parseDuration(onset); + const durationMinutes = parseDuration(duration); + const aftereffectsMinutes = parseDuration(aftereffects); + + return [ + { + phase: 'Onset', + duration: onsetMinutes, + color: theme.colors.primary, + intensity: 30, + }, + { + phase: 'Peak', + duration: durationMinutes, + color: theme.colors.secondary, + intensity: 100, + }, + { + phase: 'After Effects', + duration: aftereffectsMinutes, + color: theme.colors.tertiary, + intensity: 50, + }, + ]; + }; + + const renderTimeline = () => { + const timelineData = generateTimelineData(); + if (!timelineData) return null; + + const totalDuration = timelineData.reduce((sum, phase) => sum + phase.duration, 0); + + return ( + + + + + Duration Profile + + + {timelineData.map((phase, index) => ( + + + + + + {phase.phase} + {'\n'} + {formatDuration(phase.duration)} + + + ))} + + + + + + Onset: {drugDetails.formatted_onset?.value} + + + + + + Duration: {drugDetails.formatted_duration?.value} + + + + + + After Effects: {drugDetails.formatted_aftereffects?.value} + + + + + + ); + }; + + const renderEffectsChart = () => { + const chartData = generateChartData(); + if (!chartData) return null; + + return ( + + + Intensity Over Time + theme.colors.primary + Math.round(opacity * 255).toString(16), + labelColor: () => theme.colors.onSurface, + style: { + borderRadius: 16, + }, + propsForDots: { + r: '6', + strokeWidth: '2', + stroke: theme.colors.primary, + }, + }} + bezier + style={styles(theme).chart} + /> + + + ); + }; + + const renderEffects = () => { + if (!drugDetails.formatted_effects) return null; + + return ( + + + + + Common Effects + + + {drugDetails.formatted_effects.map((effect, index) => ( + + + {effect} + + ))} + + + + ); + }; + + const renderDoseTable = () => { + if (!drugDetails.formatted_dose) return null; + + return ( + + + + + Dosage Information + + {Object.entries(drugDetails.formatted_dose).map(([route, doses]) => ( + + {route} + + {Object.entries(doses).map(([classification, amount]) => ( + + {classification} + {amount} + + ))} + + + ))} + {drugDetails.dose_note && ( + + + {drugDetails.dose_note} + + )} + + + ); + }; + + const renderCombos = () => { + if (!drugDetails.combos) return null; + + return ( + + + + + Drug Interactions + + + {Object.entries(drugDetails.combos).map(([drug, combo]) => ( + + + + {drug} + {combo.status} + + {combo.note && ( + {combo.note} + )} + + ))} + + + + ); + }; + + const renderContent = () => { + switch (selectedTab) { + case 'overview': + return ( + <> + {renderTimeline()} + {renderEffectsChart()} + {renderEffects()} + + ); + case 'dosage': + return renderDoseTable(); + case 'interactions': + return renderCombos(); + default: + return null; + } + }; const getCategoryColor = (category: string) => { const categoryColors: { [key: string]: string } = { @@ -273,7 +566,7 @@ const DrugDetailScreen: React.FC = ({ drug, onClose }) => case 'low risk & decrease': return theme.colors.primary; case 'caution': - return MD3Colors.orange700; + return theme.colors.tertiary; case 'unsafe': case 'dangerous': return theme.colors.error; @@ -282,348 +575,158 @@ const DrugDetailScreen: React.FC = ({ drug, onClose }) => } }; - const renderDosageRow = (label: string, amount: string) => ( - - {label} - {amount} - - ); - - const chartConfig = { - backgroundGradientFrom: theme.colors.surface, - backgroundGradientTo: theme.colors.surface, - color: (opacity = 1) => theme.colors.primary + (opacity !== 1 ? opacity * 255 : ''), - labelColor: (opacity = 1) => theme.colors.onSurface, - propsForDots: { - r: '4', - strokeWidth: '2', - stroke: theme.colors.primary, - }, - propsForBackgroundLines: { - strokeDasharray: '', - stroke: theme.dark ? theme.colors.surfaceDisabled : theme.colors.surfaceVariant, - }, - decimalPlaces: 0, + const formatDuration = (minutes: number): string => { + if (minutes < 60) { + return `${Math.round(minutes)}m`; + } + const hours = Math.floor(minutes / 60); + const remainingMinutes = Math.round(minutes % 60); + return remainingMinutes > 0 + ? `${hours}h ${remainingMinutes}m` + : `${hours}h`; }; + const scrollHandler = useAnimatedScrollHandler({ + onScroll: (event) => { + scrollY.value = event.contentOffset.y; + }, + }); + if (!drugDetails) { return ( - + - - Substance details not available. - - - + Loading substance details... + ); } return ( - - - - - - - {/* Header Section with Categories and Aliases */} - - {/* Categories */} - {drugDetails.categories && drugDetails.categories.length > 0 && ( - - Categories - - {drugDetails.categories.map((category: string, index: number) => ( - - {category} - - ))} - - - )} - - {/* Aliases */} - {drugDetails.aliases && drugDetails.aliases.length > 0 && ( - - Aliases - - {drugDetails.aliases.map((alias: string, index: number) => ( - - {alias} - - ))} - - - )} - - - - - {/* Main Content */} - - {/* Summary */} - {drugDetails.properties?.summary && ( - - - - Summary - - {drugDetails.properties.summary} - - - - - )} + + + + + {}} + iconColor={theme.colors.onSurface} + /> + + - {/* Effects */} - {drugDetails.formatted_effects && drugDetails.formatted_effects.length > 0 && ( - - - - Common Effects - - {drugDetails.formatted_effects.map((effect: string, index: number) => ( - - {effect} - - ))} - - - - - )} + - {/* Effect Intensity Chart */} - {chartData && ( - - - - Effect Timeline - + + + + {drug.categories.map((category, index) => ( + + {category} + + ))} + + + {drugDetails.properties?.summary} + + {drug.aliases.length > 0 && ( + + Also known as: + + {drug.aliases.join(', ')} + + + )} + + + + {renderContent()} + + {drugDetails.links && Object.keys(drugDetails.links).length > 0 && ( + + + + + Additional Resources + + {Object.entries(drugDetails.links).map(([key, url]) => ( + Linking.openURL(url)} + > + - - - - - Onset: {drugDetails.formatted_onset?.value ?? 'Unknown'}{' '} - {drugDetails.formatted_onset?._unit ?? ''} - - - - - - Duration: {drugDetails.formatted_duration?.value ?? 'Unknown'}{' '} - {drugDetails.formatted_duration?._unit ?? ''} - - - - - - After effects: {drugDetails.formatted_aftereffects?.value ?? 'Unknown'}{' '} - {drugDetails.formatted_aftereffects?._unit ?? ''} - - - - - - - )} - - {/* Doses */} - {drugDetails.formatted_dose && Object.keys(drugDetails.formatted_dose).length > 0 && ( - - - - Dosage - {Object.entries(drugDetails.formatted_dose).map(([roa, doses], index) => ( - - {roa} - - - Strength - Amount - - {Object.entries(doses as { [key: string]: string }).map(([strength, amount]) => - renderDosageRow(strength, amount) - )} - - - ))} - {drugDetails.dose_note && ( - - {drugDetails.dose_note} - - )} - - - - )} - - {/* Combos */} - {drugDetails.combos && Object.keys(drugDetails.combos).length > 0 && ( - - - - Combinations - {Object.entries(drugDetails.combos).map(([comboDrug, details], index) => ( - - - - - {comboDrug.toUpperCase()} - - - - Status: {details.status} - - {details.note && ( - Note: {details.note} - )} - {details.sources && details.sources.length > 0 && ( - - Sources: - {details.sources.map((source: any, idx: number) => ( - Linking.openURL(source.url)} - > - - - {source.author}: {source.title} - - - ))} - - )} - - ))} - - - - )} - - {/* External Links */} - {drugDetails.links && ( - - - - External Links - {drugDetails.links.experiences && ( - - Linking.openURL(drugDetails.links!.experiences!)} - > - - Erowid Experiences - - - )} - {drugDetails.links.tihkal && ( - - Linking.openURL(drugDetails.links!.tihkal!)} - > - - TIHKAL - - - )} - - - - )} - - {/* General Sources */} - {drugDetails.sources && drugDetails.sources._general && ( - - - - General Sources - {drugDetails.sources._general.map((source: string, index: number) => ( - { - const urlMatch = source.match(/\bhttps?:\/\/\S+/gi); - if (urlMatch && urlMatch[0]) { - Linking.openURL(urlMatch[0]); - } - }} - > - - {source} - - ))} - - - - )} - - - - + + {key.charAt(0).toUpperCase() + key.slice(1)} + + + ))} + + + )} + ); }; @@ -634,190 +737,245 @@ const styles = (theme: any) => flex: 1, backgroundColor: theme.colors.background, }, + header: { + zIndex: 1, + backgroundColor: theme.colors.elevation.level2, + }, appBar: { - backgroundColor: theme.colors.surface, + elevation: 0, + backgroundColor: theme.colors.elevation.level2, }, - appBarTitle: { - fontWeight: 'bold', - color: theme.colors.onSurface, + segmentedButtons: { + margin: 16, + borderRadius: 28, }, - scrollContainer: { - padding: 16, - paddingBottom: 30, + segmentButton: { + borderRadius: 28, }, - headerSection: { - marginBottom: 10, + content: { + flex: 1, }, - title: { - fontSize: 28, - fontWeight: 'bold', - color: theme.colors.onSurface, + sectionHeader: { + flexDirection: 'row', + alignItems: 'center', marginBottom: 16, - textAlign: 'center', - marginTop: 16, + gap: 12, + }, + sectionTitle: { + color: theme.colors.onSurface, + fontSize: 20, + fontWeight: '600', }, - section: { + summaryCard: { + margin: 16, + marginTop: 0, + borderRadius: 16, + }, + categoryContainer: { + flexDirection: 'row', + flexWrap: 'wrap', + gap: 8, marginBottom: 16, }, - sectionLabel: { - color: theme.colors.secondary, - marginBottom: 8, - fontWeight: '500', + categoryChip: { + borderRadius: 16, }, - sectionTitle: { - fontSize: 18, + categoryChipText: { + color: '#FFFFFF', + }, + summary: { + fontSize: 16, + lineHeight: 24, + color: theme.colors.onSurfaceVariant, + }, + aliasesContainer: { + marginTop: 16, + flexDirection: 'row', + flexWrap: 'wrap', + }, + aliasesLabel: { fontWeight: 'bold', + marginRight: 8, color: theme.colors.onSurface, - marginBottom: 8, }, - cardTitle: { - color: theme.colors.onSurface, - marginBottom: 12, + aliases: { + flex: 1, + color: theme.colors.onSurfaceVariant, }, - subSectionTitle: { - fontSize: 16, - fontWeight: '600', - color: theme.colors.onSurface, - marginTop: 8, - marginBottom: 4, + timelineCard: { + margin: 16, + borderRadius: 16, }, - chipContainer: { + timeline: { flexDirection: 'row', - flexWrap: 'wrap', - justifyContent: 'flex-start', - marginBottom: 8, + height: 100, + gap: 8, }, - chip: { - marginRight: 6, - marginBottom: 6, - backgroundColor: theme.colors.surfaceVariant, - height: 'auto', - justifyContent: 'center', - paddingHorizontal: 10, - paddingVertical: 4, - }, - effectChip: { - marginRight: 6, - marginBottom: 6, - height: 'auto', + timelinePhase: { + alignItems: 'center', + }, + timelineBar: { + flex: 1, + width: '100%', justifyContent: 'center', - paddingHorizontal: 10, - paddingVertical: 4, }, - chipText: { - color: theme.colors.onSurface, - fontSize: 12, - textAlign: 'center', - flexWrap: 'wrap', + progressBar: { + height: 8, + borderRadius: 4, }, - divider: { - backgroundColor: theme.colors.outline, - marginVertical: 16, - height: 0.5, + timelineLabel: { + fontSize: 12, + textAlign: 'center', + marginTop: 8, + color: theme.colors.onSurfaceVariant, }, - card: { - marginVertical: 8, - borderRadius: 12, - backgroundColor: theme.colors.surface, - overflow: 'hidden', + timelineInfo: { + marginTop: 16, + gap: 8, }, - paragraph: { - color: theme.colors.onSurface, + infoItem: { + flexDirection: 'row', + alignItems: 'center', + gap: 8, + }, + infoText: { + color: theme.colors.onSurfaceVariant, fontSize: 14, - lineHeight: 20, }, - chartStyle: { - marginVertical: 8, - borderRadius: 8, + effectsCard: { + margin: 16, + borderRadius: 16, }, - durationInfo: { - marginTop: 12, + effectsGrid: { + flexDirection: 'row', + flexWrap: 'wrap', + gap: 12, }, - durationItem: { + effectItem: { flexDirection: 'row', alignItems: 'center', - marginBottom: 4, - }, - durationIcon: { - backgroundColor: theme.colors.surfaceVariant, - marginRight: 8, + gap: 4, + width: '45%', }, - durationText: { + effectText: { color: theme.colors.onSurface, - fontSize: 12, + fontSize: 14, + flex: 1, + }, + doseCard: { + margin: 16, + borderRadius: 16, }, doseSection: { - marginTop: 8, + marginBottom: 24, }, - comboCard: { + doseRoute: { + color: theme.colors.primary, + fontSize: 16, + fontWeight: '600', + marginBottom: 8, + }, + doseGrid: { + flexDirection: 'row', + flexWrap: 'wrap', + gap: 16, + }, + doseItem: { + flex: 1, + minWidth: '45%', backgroundColor: theme.colors.surfaceVariant, - marginBottom: 12, padding: 12, - borderLeftWidth: 4, - borderRadius: 8, + borderRadius: 12, }, - comboHeader: { + doseClassification: { + color: theme.colors.onSurfaceVariant, + fontSize: 12, + marginBottom: 4, + }, + doseAmount: { + color: theme.colors.onSurface, + fontSize: 16, + fontWeight: '600', + }, + doseNote: { flexDirection: 'row', alignItems: 'center', - marginBottom: 4, + gap: 8, + marginTop: 16, + padding: 12, + backgroundColor: theme.colors.errorContainer, + borderRadius: 12, }, - comboIcon: { - backgroundColor: 'transparent', - marginRight: 8, + doseNoteText: { + color: theme.colors.onErrorContainer, + fontSize: 14, + flex: 1, }, - comboDrugName: { - color: theme.colors.onSurface, + combosCard: { + margin: 16, + borderRadius: 16, + }, + combosGrid: { + gap: 12, + }, + comboItem: { + padding: 16, + borderRadius: 12, + }, + comboHeader: { + flexDirection: 'row', + alignItems: 'center', + gap: 12, + }, + comboDrug: { + flex: 1, + color: theme.colors.surface, fontSize: 16, - fontWeight: 'bold', + fontWeight: '600', }, comboStatus: { - color: theme.colors.onSurface, - fontSize: 14, - marginBottom: 4, + backgroundColor: 'rgba(255,255,255,0.2)', }, comboNote: { - color: theme.colors.onSurfaceVariant, - fontSize: 12, - fontStyle: 'italic', - }, - sourcesSection: { marginTop: 8, - }, - sourcesTitle: { - color: theme.colors.onSurface, + color: theme.colors.surface, fontSize: 14, - marginBottom: 4, }, - sourceLink: { - color: theme.colors.primary, - fontSize: 13, - marginLeft: 8, - marginBottom: 2, + linksCard: { + margin: 16, + borderRadius: 16, }, - linkButton: { + linkItem: { flexDirection: 'row', alignItems: 'center', - paddingVertical: 8, + gap: 12, + paddingVertical: 12, }, linkText: { color: theme.colors.primary, - fontSize: 14, - marginLeft: 8, + fontSize: 16, }, - closeButton: { + loadingContainer: { + flex: 1, + justifyContent: 'center', alignItems: 'center', - marginTop: 20, }, - closeButtonText: { + loadingText: { + marginTop: 16, color: theme.colors.onSurface, - fontSize: 16, - marginTop: 4, }, - fab: { - position: 'absolute', + chartCard: { margin: 16, - right: 0, - bottom: 0, + borderRadius: 16, + backgroundColor: theme.colors.surface, + }, + chartTitle: { + marginBottom: 16, + color: theme.colors.onSurface, + }, + chart: { + borderRadius: 8, + marginVertical: 8, }, }); diff --git a/app/index.tsx b/app/index.tsx index 5f257af..a636657 100644 --- a/app/index.tsx +++ b/app/index.tsx @@ -4,6 +4,10 @@ import { Provider as PaperProvider } from 'react-native-paper'; import { GestureHandlerRootView } from 'react-native-gesture-handler'; // Import GestureHandlerRootView import { lightTheme, darkTheme } from './themes'; import HomeScreen from './pages/HomeScreen'; +import { createSharedElementStackNavigator } from 'react-navigation-shared-element'; +import DrugDetailScreen from './components/DrugDetail'; + +const Stack = createSharedElementStackNavigator(); export default function Index() { const scheme = useColorScheme(); @@ -12,9 +16,7 @@ export default function Index() { return ( - {/* - // @ts-ignore */} - + ); diff --git a/app/pages/AboutRoute.tsx b/app/pages/AboutRoute.tsx index 65ae026..2490f92 100644 --- a/app/pages/AboutRoute.tsx +++ b/app/pages/AboutRoute.tsx @@ -1,251 +1,86 @@ -import React, { useEffect, useState } from 'react'; -import { ScrollView, View, StyleSheet, Text, Linking, TouchableOpacity } from 'react-native'; -import { Avatar, Card, Title, Paragraph, Divider, useTheme, Button, List, IconButton } from 'react-native-paper'; -import { MaterialCommunityIcons } from '@expo/vector-icons'; -import * as Animatable from 'react-native-animatable'; -import AsyncStorage from '@react-native-async-storage/async-storage'; -import moment from 'moment'; - -const AboutRoute = () => { +import React from 'react'; +import { View, StyleSheet, ScrollView } from 'react-native'; +import { + Text, + Card, + useTheme, + Title, + Surface +} from 'react-native-paper'; +import { ThemedView } from '../../components/ThemedView'; + +export default function AboutRoute() { const theme = useTheme(); - const isDarkMode = theme.dark; - - const [lastUpdatedDrugs, setLastUpdatedDrugs] = useState(null); - const [lastUpdatedCombos, setLastUpdatedCombos] = useState(null); - - useEffect(() => { - const fetchLastUpdated = async () => { - const [drugsUpdated, combosUpdated] = await Promise.all([ - AsyncStorage.getItem('lastUpdatedDrugs'), - AsyncStorage.getItem('lastUpdatedCombos'), - ]); - - setLastUpdatedDrugs(drugsUpdated); - setLastUpdatedCombos(combosUpdated); - }; - - fetchLastUpdated(); - }, []); - - const styles = StyleSheet.create({ - container: { - flexGrow: 1, - padding: 16, - backgroundColor: theme.colors.background, - }, - headerContainer: { - alignItems: 'center', - marginBottom: 24, - }, - headerIcon: { - marginBottom: 16, - marginTop: 20, - }, - title: { - color: theme.colors.onBackground, - fontSize: 28, - fontWeight: '700', - textAlign: 'center', - }, - sectionHeader: { - color: theme.colors.onBackground, - fontSize: 22, - fontWeight: '600', - marginTop: 24, - marginBottom: 12, - }, - paragraph: { - color: theme.colors.onSurface, - marginBottom: 12, - lineHeight: 22, - fontSize: 16, - }, - link: { - color: theme.colors.primary, - textDecorationLine: 'underline', - }, - iconTextContainer: { - flexDirection: 'row', - alignItems: 'center', - marginBottom: 8, - }, - icon: { - marginRight: 8, - }, - card: { - marginVertical: 8, - borderRadius: 12, - elevation: 4, - backgroundColor: theme.colors.surface, - padding: 16, - }, - footerText: { - textAlign: 'center', - color: theme.colors.onSurfaceVariant, - marginTop: 24, - fontSize: 14, - }, - socialIconsContainer: { - flexDirection: 'row', - justifyContent: 'center', - marginTop: 16, - }, - socialIconButton: { - marginHorizontal: 8, - }, - }); - - // Function to handle link presses - const handleLinkPress = async (url: string) => { - const supported = await Linking.canOpenURL(url); - if (supported) { - await Linking.openURL(url); - } else { - console.warn(`Don't know how to open URI: ${url}`); - } - }; - - const formatDate = (dateString: string | null) => { - if (!dateString) return 'Unknown'; - return moment(dateString).format('MMMM Do YYYY, h:mm:ss a'); - }; return ( - - {/* Header Section */} - - - About TripSit - - - {/* About TripSit */} - - + + + - {/* Last Updated Information */} - Data Last Updated - - Substances Data: {formatDate(lastUpdatedDrugs)} - - - Combinations Data: {formatDate(lastUpdatedCombos)} - - - - - TripSit is a harm reduction organization dedicated to providing factual and unbiased information about various substances. Our mission is to promote safe and informed choices through education, support, and community engagement. - - - - {/* Our Services */} - Our Services - } - onPress={() => handleLinkPress('https://tripsit.me/chat')} - /> - } - onPress={() => handleLinkPress('https://tripsit.me/charts')} - /> - } - onPress={() => handleLinkPress('https://tripsit.me/substances')} - /> - } - onPress={() => handleLinkPress('https://tripsit.me/forums')} - /> - - - {/* Our Mission */} - Our Mission - - Our mission is to reduce the harm associated with substance use by providing accurate information, support, and resources. We believe that through education and community, individuals can make safer and more informed decisions. - - - - {/* Contact Us */} - Contact Us - - - - handleLinkPress('mailto:support@tripsit.me')}> - support@tripsit.me - - - - - handleLinkPress('https://twitter.com/TripSit')}> - @TripSit - - - - - handleLinkPress('https://facebook.com/TripSit')}> - facebook.com/TripSit - - - - - handleLinkPress('https://tripsit.me')}> - https://tripsit.me - - - + About TripSit + + + TripSit began in 2011 as an IRC channel providing drug safety and harm reduction services, allowing people to chat + anonymously about drugs in a safe environment. + + + + + TripSit, one of the oldest harm reduction communities, helps by providing factual drug information and a + supportive community. + + + + + Our mission is supporting responsible drug use through education and harm reduction strategies. We aim to reduce + adverse consequences without judgment. + + + + + TripSit remains independent and community-funded, with passionate volunteers providing 24/7 support through our chat + networks, website, apps, and other tools. + + + + + If you encounter a substance-related crisis situation, please visit chat.tripsit.me for immediate help from + our trained volunteer team. + + + + + TripSit aims to provide objective drug information and support, combined with a warm, welcoming community that helps + promote safer drug use. + + - - - {/* Social Media Icons */} - - - } - size={36} - onPress={() => handleLinkPress('https://twitter.com/TripSit')} - style={styles.socialIconButton} - /> - } - size={36} - onPress={() => handleLinkPress('https://facebook.com/TripSit')} - style={styles.socialIconButton} - /> - } - size={36} - onPress={() => handleLinkPress('mailto:support@tripsit.me')} - style={styles.socialIconButton} - /> - } - size={36} - onPress={() => handleLinkPress('https://tripsit.me')} - style={styles.socialIconButton} - /> - - - - {/* Footer */} - - © {new Date().getFullYear()} TripSit. All rights reserved. - Developed by Sympact06 - - + + ); -}; - -export default AboutRoute; +} + +const styles = StyleSheet.create({ + container: { + flex: 1, + }, + scrollContent: { + padding: 16, + }, + card: { + marginBottom: 16, + }, + surface: { + padding: 8, + marginVertical: 8, + borderRadius: 8, + }, + title: { + marginBottom: 16, + textAlign: 'center', + }, + text: { + lineHeight: 20, + }, +}); diff --git a/app/pages/CombosRoute.tsx b/app/pages/CombosRoute.tsx index 12f2d60..57c3405 100644 --- a/app/pages/CombosRoute.tsx +++ b/app/pages/CombosRoute.tsx @@ -1,3 +1,4 @@ +/* eslint-disable sonarjs/no-all-duplicated-branches */ import React, { useState, useEffect } from 'react'; import { View, @@ -15,7 +16,6 @@ import { Appbar, Avatar, Text, - Chip, Portal, Modal, Snackbar, @@ -32,10 +32,16 @@ import DrugDetailScreen from '../components/DrugDetail'; import localCombosData from '../data/combos.json'; import localComboDefinitions from '../data/combo_definitions.json'; +type Source = { + author: string; + title: string; + url: string; +}; + type Interaction = { status: string; note?: string; - sources?: any[]; + sources?: Source[]; }; type CombosData = { @@ -77,8 +83,18 @@ type Drug = { details: DrugDetail; }; +// Constants const COMBOS_URL = 'https://raw.githubusercontent.com/TripSit/drugs/main/combos.json'; const COMBO_DEFINITIONS_URL = 'https://raw.githubusercontent.com/TripSit/drugs/main/combo_definitions.json'; +const DEFAULT_COLOR = '#9E9E9E'; +const DEFAULT_DARK_BG = '#1F1F1F'; +const DEFAULT_SUMMARY = ''; +const ICON_CHECK_OUTLINE = 'check-circle-outline'; +const ICON_CHECK = 'check-circle'; +const ICON_ALERT = 'alert-circle-outline'; +const ICON_CLOSE = 'close-circle-outline'; +const ICON_DANGER = 'alert-octagon'; +const ICON_HELP = 'help-circle-outline'; const CombosRoute: React.FC = () => { const [searchQuery, setSearchQuery] = useState(''); @@ -131,7 +147,7 @@ const CombosRoute: React.FC = () => { id: `${index}`, name: drugName, pretty_name: capitalizeFirstLetter(drugName), - summary: '', // Provide a default summary or fetch it if available + summary: DEFAULT_SUMMARY, // Provide a default summary or fetch it if available aliases: [], categories: [], // Populate if available details: { @@ -140,7 +156,7 @@ const CombosRoute: React.FC = () => { aliases: [], categories: [], properties: { - summary: '', // Provide actual summary if available + summary: DEFAULT_SUMMARY, // Provide actual summary if available }, }, })); @@ -176,7 +192,7 @@ const CombosRoute: React.FC = () => { id: `${index}`, name: drugName, pretty_name: capitalizeFirstLetter(drugName), - summary: '', // Provide actual summary if available + summary: DEFAULT_SUMMARY, // Provide actual summary if available aliases: [], categories: [], // Populate if available details: { @@ -185,7 +201,7 @@ const CombosRoute: React.FC = () => { aliases: [], categories: [], properties: { - summary: '', // Provide actual summary if available + summary: DEFAULT_SUMMARY, // Provide actual summary if available }, }, })); @@ -238,7 +254,7 @@ const CombosRoute: React.FC = () => { useEffect(() => { fetchInteractionResult(); - }, [selectedDrugs]); + }, [selectedDrugs, combosData]); const getStatusColor = (status: string) => { switch (status.toLowerCase()) { @@ -251,23 +267,25 @@ const CombosRoute: React.FC = () => { case 'dangerous': return '#F44336'; // Red default: - return '#9E9E9E'; // Grey + return DEFAULT_COLOR; // Grey } }; const getStatusIcon = (status: string) => { + if (!status) return ICON_HELP; switch (status.toLowerCase()) { case 'low risk & no synergy': - return 'check-circle-outline'; + return ICON_CHECK_OUTLINE; case 'low risk & synergy': - return 'check-circle'; + return ICON_CHECK; case 'caution': - return 'alert-circle-outline'; + return ICON_ALERT; case 'unsafe': + return ICON_CLOSE; case 'dangerous': - return 'close-circle-outline'; + return ICON_DANGER; default: - return 'help-circle-outline'; + return ICON_HELP; } }; @@ -316,7 +334,7 @@ const CombosRoute: React.FC = () => { }).start(); }; - const iconName = categoryIcons[item.categories[0]?.toLowerCase()] || 'pill'; + const iconName = categoryIcons[item.categories[0]?.toLowerCase()] ?? categoryIcons.default; return ( @@ -351,133 +369,92 @@ const CombosRoute: React.FC = () => { }; const renderInteractionResult = () => { - if (selectedDrugs.length === 2 && interactionResult) { + if (selectedDrugs.length === 2) { const [drug1, drug2] = selectedDrugs; - const status = interactionResult.status; - const statusColor = getStatusColor(status); - const statusIcon = getStatusIcon(status); + const statusText = interactionResult ? interactionResult.status : 'No interaction data available'; + const statusColor = interactionResult ? getStatusColor(interactionResult.status) : DEFAULT_COLOR; return ( setIsModalVisible(true)}> - + {/* eslint-disable-next-line sonarjs/no-all-duplicated-branches */} + {drug1.pretty_name} + {drug2.pretty_name} + {/* eslint-disable-next-line sonarjs/no-all-duplicated-branches */} - {status} - - - - - - - ); - } else if (selectedDrugs.length === 2 && !interactionResult) { - return ( - - setIsModalVisible(true)}> - - - - - {selectedDrugs[0].pretty_name} + {selectedDrugs[1].pretty_name} - - - No interaction data available + {statusText} - + ); - } else { - return null; } + return null; }; const renderInteractionModal = () => { - if (selectedDrugs.length === 2 && interactionResult) { - const [drug1, drug2] = selectedDrugs; - const status = interactionResult.status; - const statusColor = getStatusColor(status); - const definitionObj = comboDefinitions.find( - def => def.status.toLowerCase() === status.toLowerCase() - ); - const definition = definitionObj ? definitionObj.definition : 'No definition available for this status.'; - const note = interactionResult.note; + if (!selectedDrugs || selectedDrugs.length !== 2) { + return null; + } - return ( - setIsModalVisible(false)} - contentContainerStyle={[ - styles(isDarkMode).modalContainer, - { backgroundColor: isDarkMode ? '#1F1F1F' : '#FFFFFF' }, - ]} - > - - setIsModalVisible(false)} /> - - - - - - - {status} - - - {note && {note}} - {definition} - - - - ); - } else if (selectedDrugs.length === 2 && !interactionResult) { - const [drug1, drug2] = selectedDrugs; - return ( - setIsModalVisible(false)} - contentContainerStyle={[ - styles(isDarkMode).modalContainer, - { backgroundColor: isDarkMode ? '#1F1F1F' : '#FFFFFF' }, - ]} - > - - setIsModalVisible(false)} /> - - - - - - - No interaction data available - - - - Please exercise caution and consult additional resources. - + const [drug1, drug2] = selectedDrugs; + const modalContentStyle = [ + styles(isDarkMode).modalContainer, + { backgroundColor: isDarkMode ? DEFAULT_DARK_BG : '#FFFFFF' } + ]; + + return ( + setIsModalVisible(false)} + contentContainerStyle={modalContentStyle} + > + + setIsModalVisible(false)} /> + + + + + {interactionResult ? ( + <> + + + {interactionResult.status} + + {interactionResult.note && ( + {interactionResult.note} + )} + + {comboDefinitions.find( + def => def.status.toLowerCase() === interactionResult.status.toLowerCase() + )?.definition || 'No definition available for this status.'} + + + ) : ( + <> + + + No interaction data available + + + Please exercise caution and consult additional resources. + + + )} - - - ); - } else { - return null; - } + + + + ); }; if (loading) { diff --git a/app/pages/FactsRoute.tsx b/app/pages/FactsRoute.tsx index 9ac38e2..af89407 100644 --- a/app/pages/FactsRoute.tsx +++ b/app/pages/FactsRoute.tsx @@ -1,6 +1,6 @@ // FactsRoute.tsx -import React, { useState, useEffect } from 'react'; +import React, { useState, useEffect, useCallback } from 'react'; import { View, FlatList, @@ -8,6 +8,12 @@ import { ActivityIndicator, TouchableOpacity, useColorScheme, + Dimensions, + BackHandler, + Linking, + ScrollView, + NativeScrollEvent, + NativeSyntheticEvent, } from 'react-native'; import { Searchbar, @@ -21,11 +27,38 @@ import { Portal, Modal, Button, + useTheme, + IconButton, + Surface, + FAB, + Divider, + Title, + MD3Colors, + Tooltip, + AnimatedFAB, + SegmentedButtons, + Badge, + ProgressBar, } from 'react-native-paper'; import AsyncStorage from '@react-native-async-storage/async-storage'; import NetInfo from '@react-native-community/netinfo'; +import Animated, { + FadeIn, + FadeOut, + SlideInRight, + SlideOutLeft, + withSpring, + useAnimatedStyle, + useSharedValue, + withTiming, + interpolate, + Extrapolate, + useAnimatedScrollHandler, +} from 'react-native-reanimated'; +import { useSafeAreaInsets } from 'react-native-safe-area-context'; import localDrugData from '../data/drugs.json'; import DrugDetailScreen from '../components/DrugDetail'; +import { formatDistance } from 'date-fns'; type Combo = { status: string; @@ -37,6 +70,33 @@ type Combo = { }[]; }; +type FormattedTiming = { + _unit: string; + value?: string; + Insufflated?: string; + Oral?: string; + [key: string]: string | undefined; +}; + +type DrugProperties = { + summary?: string; + after_effects?: string; + avoid?: string; + half_life?: string; + marquis?: string; + aliases?: string[]; + categories?: string[]; + dose?: string; + onset?: string; + [key: string]: string | string[] | undefined; +}; + +type DrugSources = { + _general?: string[]; + bioavailability?: string[]; + [key: string]: string[] | undefined; +}; + type DrugDetail = { name: string; pretty_name?: string; @@ -44,21 +104,16 @@ type DrugDetail = { categories?: string[]; combos?: { [key: string]: Combo }; dose_note?: string; - formatted_aftereffects?: { _unit: string; value: string }; + formatted_aftereffects?: any; formatted_dose?: { [key: string]: { [key: string]: string } }; - formatted_duration?: { _unit: string; value: string }; + formatted_duration?: any; formatted_effects?: string[]; - formatted_onset?: { _unit: string; value: string }; - links?: { experiences?: string; tihkal?: string }; - properties?: { - summary?: string; - after_effects?: string; - avoid?: string; - half_life?: string; - marquis?: string; - }; + formatted_onset?: any; + links?: { [key: string]: string }; + properties?: DrugProperties; pweffects?: { [key: string]: string }; - sources?: { _general?: string[] }; + sources?: DrugSources; + [key: string]: any; // Allow additional properties }; type Drug = { @@ -67,9 +122,65 @@ type Drug = { summary: string; categories: string[]; aliases: string[]; - details: DrugDetail; -} + details: DrugDetail; +}; + +type ApiDrugObject = { + [key: string]: Omit & { name?: string }; +}; +type ApiResponse = ApiDrugObject[]; + +const transformToRequired = (detail: DrugDetail, drugName: string): DrugDetail => { + return { + ...detail, + name: detail.name || drugName, + }; +}; + +const parseApiDrug = (drugName: string, drugDetails: ApiDrugObject[string], index: number, innerIndex: number): Drug => ({ + id: `${index}-${innerIndex}`, + name: drugDetails.pretty_name || drugName, + summary: drugDetails.properties?.summary || 'No summary available.', + categories: drugDetails.categories || ['Uncategorized'], + aliases: drugDetails.aliases || [], + details: { + ...drugDetails, + name: drugName, + }, +}); + +const parseLocalDrug = (drugName: string, drugDetails: ApiDrugObject[string], index: number): Drug => ({ + id: `${index}`, + name: drugDetails.pretty_name || drugName, + summary: drugDetails.properties?.summary || 'No summary available.', + categories: drugDetails.categories || ['Uncategorized'], + aliases: drugDetails.aliases || [], + details: { + ...drugDetails, + name: drugName, + }, +}); + +const parseApiData = (data: ApiResponse): Drug[] => { + const drugs: Drug[] = []; + data.forEach((drugObject, index) => { + Object.entries(drugObject).forEach(([drugName, drugDetails], innerIndex) => { + drugs.push(parseApiDrug(drugName, drugDetails, index, innerIndex)); + }); + }); + return drugs; +}; + +const parseLocalData = (data: Record): Drug[] => { + return Object.entries(data).map(([drugName, drugDetails], index) => + parseLocalDrug(drugName, drugDetails, index) + ); +}; + +const AnimatedSurface = Animated.createAnimatedComponent(Surface); +const AnimatedCard = Animated.createAnimatedComponent(Card); +const AnimatedScrollView = Animated.createAnimatedComponent(ScrollView); const FactsRoute: React.FC = () => { const [data, setData] = useState([]); @@ -86,43 +197,62 @@ const FactsRoute: React.FC = () => { const ITEMS_PER_PAGE = 20; const [page, setPage] = useState(1); + const theme = useTheme(); + const insets = useSafeAreaInsets(); + const windowHeight = Dimensions.get('window').height; + + // Animation values + const searchBarHeight = useSharedValue(0); + const filterModalY = useSharedValue(windowHeight); + const headerOpacity = useSharedValue(1); + const listScale = useSharedValue(1); + + const searchBarAnimatedStyle = useAnimatedStyle(() => ({ + opacity: interpolate(searchBarHeight.value, [0, 56], [0, 1], Extrapolate.CLAMP), + transform: [ + { + translateY: interpolate(searchBarHeight.value, [0, 56], [-20, 0], Extrapolate.CLAMP), + }, + ], + height: searchBarHeight.value, + overflow: 'hidden', + })); + + const filterModalAnimatedStyle = useAnimatedStyle(() => ({ + transform: [{ translateY: filterModalY.value }], + })); + + const toggleSearchBar = () => { + if (searchBarHeight.value === 0) { + searchBarHeight.value = withSpring(56); + headerOpacity.value = withTiming(0.8); + listScale.value = withTiming(0.98); + } else { + searchBarHeight.value = withSpring(0); + headerOpacity.value = withTiming(1); + listScale.value = withTiming(1); + setSearchQuery(''); + applyFilters('', selectedCategories); + } + setIsSearchBarVisible(!isSearchBarVisible); + }; + + const toggleFilterModal = () => { + if (filterModalY.value === windowHeight) { + filterModalY.value = withSpring(windowHeight * 0.4); + listScale.value = withTiming(0.95); + } else { + filterModalY.value = withSpring(windowHeight); + listScale.value = withTiming(1); + } + setIsFilterModalVisible(!isFilterModalVisible); + }; + useEffect(() => { let isMounted = true; - const parseDrugData = (data: any): Drug[] => { - const drugs: Drug[] = []; - - if (Array.isArray(data)) { - // Data from API - data.forEach((drugObject: any, index: number) => { - Object.keys(drugObject).forEach((drugName, innerIndex) => { - const drugDetails = drugObject[drugName]; - drugs.push({ - id: `${index}-${innerIndex}`, - name: drugDetails.pretty_name || drugName, - summary: drugDetails.properties?.summary || 'No summary available.', - categories: drugDetails.categories || ['Uncategorized'], - aliases: drugDetails.aliases || [], - details: drugDetails, // Store full details - }); - }); - }); - } else if (typeof data === 'object') { - // Data from local JSON - Object.keys(data).forEach((drugName, index) => { - const drugDetails = data[drugName]; - drugs.push({ - id: `${index}`, - name: drugDetails.pretty_name || drugName, - summary: drugDetails.properties?.summary || 'No summary available.', - categories: drugDetails.categories || ['Uncategorized'], - aliases: drugDetails.aliases || [], - details: drugDetails, // Store full details - }); - }); - } - - return drugs; + const parseDrugData = (data: ApiResponse | Record): Drug[] => { + return Array.isArray(data) ? parseApiData(data) : parseLocalData(data); }; const loadData = async () => { @@ -324,57 +454,76 @@ const FactsRoute: React.FC = () => { return '#757575'; }; - const renderDrug = ({ item }: { item: Drug }) => { - const categoriesArray = item.categories; - const icon = getCategoryIcon(categoriesArray); - const color = getCategoryColor(categoriesArray); + const renderDrug = useCallback(({ item, index }: { item: Drug; index: number }) => { + const categories = item.categories; + const icon = getCategoryIcon(categories); + const color = getCategoryColor(categories); return ( - setSelectedDrug(item)}> - - ( - - )} - /> - - - {categoriesArray.map((category, index) => ( - - {category} - - ))} - - - {item.summary.length > 150 ? `${item.summary.slice(0, 150)}...` : item.summary} - - - - + + setSelectedDrug(item)}> + + ( + + )} + /> + + + {categories.map((category, idx) => ( + handleCategorySelection(category.toLowerCase())} + > + {category} + + ))} + + + {item.summary.length > 150 + ? `${item.summary.slice(0, 150)}...` + : item.summary} + + + + + ); - }; + }, [isDarkMode, theme]); + + const listAnimatedStyle = useAnimatedStyle(() => ({ + transform: [{ scale: listScale.value }], + })); if (loading) { return ( - - - Loading... + + + Loading... ); } @@ -387,168 +536,304 @@ const FactsRoute: React.FC = () => { ); } - const allCategories = Array.from( - new Set(data.flatMap(drug => drug.categories.map(cat => cat.toLowerCase()))), - ); - return ( - - - - setIsFilterModalVisible(true)} - /> - setIsSearchBarVisible(!isSearchBarVisible)} - /> - - {isSearchBarVisible && ( + + + + + + + + + + { - setIsSearchBarVisible(false); - setSearchQuery(''); - applyFilters('', selectedCategories); - }} + style={styles(isDarkMode, theme).searchBar} + inputStyle={{ color: isDarkMode ? theme.colors.onSurface : theme.colors.onSurface }} /> - )} - item.id} - renderItem={renderDrug} - contentContainerStyle={styles(isDarkMode).listContainer} - onEndReached={loadMoreData} - onEndReachedThreshold={0.5} - ListFooterComponent={ - displayedData.length < filteredData.length ? ( - - ) : null - } - /> + + + + {loading ? ( + + + Loading... + + ) : ( + item.id} + contentContainerStyle={styles(isDarkMode, theme).listContainer} + onEndReached={loadMoreData} + onEndReachedThreshold={0.5} + ListFooterComponent={ + displayedData.length < filteredData.length ? ( + + ) : null + } + /> + )} + + - setIsFilterModalVisible(false)} - contentContainerStyle={[ - styles(isDarkMode).modalContainer, - { backgroundColor: isDarkMode ? '#1F1F1F' : '#FFFFFF' }, + - Filter by Categories - item} - renderItem={({ item }) => ( - - handleCategorySelection(item)} - color={categoryColors[item] || '#757575'} - /> - {item} - - )} + - - + + + + + + + Filter by Category + + + + + + {Array.from( + new Set(data.flatMap((drug) => drug.categories.map((cat) => cat.toLowerCase()))) + ).sort().map((category) => ( + handleCategorySelection(category)} + style={[ + styles(isDarkMode, theme).filterChip, + selectedCategories.includes(category) && { + backgroundColor: theme.colors.primaryContainer, + }, + ]} + textStyle={{ + color: selectedCategories.includes(category) + ? theme.colors.onPrimaryContainer + : theme.colors.onSurfaceVariant + }} + showSelectedOverlay + > + {category.charAt(0).toUpperCase() + category.slice(1)} + + ))} + + + + + + + + + + {selectedDrug && ( + + setSelectedDrug(null)} + contentContainerStyle={styles(isDarkMode, theme).modalContent} + > + setSelectedDrug(null)} + /> + + + )} ); }; -const styles = (isDarkMode: boolean) => +const styles = (isDarkMode: boolean, theme: any) => StyleSheet.create({ container: { flex: 1, - backgroundColor: isDarkMode ? '#121212' : '#FAFAFA', + backgroundColor: isDarkMode ? theme.colors.background : theme.colors.background, + }, + header: { + zIndex: 1, }, appBar: { - backgroundColor: isDarkMode ? '#1F1F1F' : '#6200EE', + elevation: 0, + }, + searchBarContainer: { + paddingHorizontal: 16, + paddingVertical: 8, + backgroundColor: 'transparent', }, searchBar: { - marginHorizontal: 10, - marginVertical: 10, - borderRadius: 8, + borderRadius: 28, elevation: 2, - backgroundColor: isDarkMode ? '#1F1F1F' : '#FFFFFF', + backgroundColor: isDarkMode ? theme.colors.elevation.level2 : theme.colors.surface, }, listContainer: { - paddingHorizontal: 10, - paddingBottom: 10, + padding: 16, + paddingBottom: 80, }, card: { - marginVertical: 8, - borderRadius: 8, - elevation: 2, - backgroundColor: isDarkMode ? '#1F1F1F' : '#FFFFFF', - borderLeftWidth: 5, + marginBottom: 16, + borderRadius: 12, + borderLeftWidth: 4, + backgroundColor: isDarkMode ? theme.colors.elevation.level2 : theme.colors.surface, + overflow: 'hidden', }, title: { - color: isDarkMode ? '#FFFFFF' : '#000000', - fontWeight: 'bold', + color: isDarkMode ? theme.colors.onSurface : theme.colors.onSurface, + fontSize: 18, + fontWeight: '600', }, chipContainer: { flexDirection: 'row', flexWrap: 'wrap', - marginVertical: 8, + gap: 8, + marginBottom: 12, }, chip: { - marginRight: 6, - marginBottom: 6, - backgroundColor: '#757575', + borderRadius: 16, }, chipText: { color: '#FFFFFF', fontSize: 12, }, summary: { - color: isDarkMode ? '#DDDDDD' : '#424242', + color: isDarkMode ? theme.colors.onSurfaceVariant : theme.colors.onSurfaceVariant, + fontSize: 14, + lineHeight: 20, }, loadingContainer: { flex: 1, justifyContent: 'center', alignItems: 'center', - backgroundColor: isDarkMode ? '#121212' : '#FFFFFF', }, loadingText: { - marginTop: 10, - color: isDarkMode ? '#FFFFFF' : '#000000', + marginTop: 16, + color: isDarkMode ? theme.colors.onSurface : theme.colors.onSurface, }, - modalContainer: { - margin: 20, - padding: 20, - borderRadius: 8, + filterModalOverlay: { + ...StyleSheet.absoluteFillObject, + backgroundColor: 'black', }, - modalTitle: { - fontSize: 18, - fontWeight: 'bold', - marginBottom: 10, - color: isDarkMode ? '#FFFFFF' : '#000000', + filterModal: { + position: 'absolute', + left: 0, + right: 0, + bottom: 0, + backgroundColor: isDarkMode ? theme.colors.elevation.level2 : theme.colors.surface, + borderTopLeftRadius: 28, + borderTopRightRadius: 28, + padding: 24, + paddingTop: 12, + elevation: 8, + maxHeight: '70%', }, - checkboxContainer: { + filterModalHandle: { + width: 32, + height: 4, + backgroundColor: isDarkMode ? 'rgba(255,255,255,0.1)' : 'rgba(0,0,0,0.1)', + borderRadius: 2, + alignSelf: 'center', + marginBottom: 12, + }, + filterModalContent: { + flex: 1, + }, + filterModalHeader: { flexDirection: 'row', + justifyContent: 'space-between', alignItems: 'center', + marginBottom: 16, + }, + filterTitle: { + color: isDarkMode ? theme.colors.onSurface : theme.colors.onSurface, + fontWeight: '600', + }, + filterScroll: { + flex: 1, + marginBottom: 16, + }, + categoriesContainer: { + flexDirection: 'row', + flexWrap: 'wrap', + gap: 8, + }, + filterChip: { marginBottom: 8, + backgroundColor: isDarkMode ? theme.colors.elevation.level3 : theme.colors.surfaceVariant, }, - checkboxLabel: { - marginLeft: 8, - color: isDarkMode ? '#FFFFFF' : '#000000', - textTransform: 'capitalize', + filterActions: { + flexDirection: 'row', + gap: 12, + marginTop: 'auto', + }, + clearButton: { + flex: 1, }, applyButton: { - marginTop: 10, + flex: 1, + }, + modalContent: { + flex: 1, + backgroundColor: isDarkMode ? theme.colors.elevation.level2 : theme.colors.surface, + margin: 0, }, }); diff --git a/app/pages/HomeScreen.tsx b/app/pages/HomeScreen.tsx index 718765b..adbe98a 100644 --- a/app/pages/HomeScreen.tsx +++ b/app/pages/HomeScreen.tsx @@ -1,113 +1,184 @@ import * as React from 'react'; -import { StyleSheet, Platform } from 'react-native'; -import { BottomNavigation, useTheme as usePaperTheme } from 'react-native-paper'; +import { StyleSheet, Platform, View, Text } from 'react-native'; +import { BottomNavigation, useTheme as usePaperTheme, TouchableRipple, Surface } from 'react-native-paper'; import { MaterialCommunityIcons } from '@expo/vector-icons'; import { routes } from '../routes'; import FactsRoute from './FactsRoute'; import CombosRoute from './CombosRoute'; import ChatRoute from './ChatRoute'; import AboutRoute from './AboutRoute'; +import Animated from 'react-native'; interface Route { key: string; title: string; - icon: string; + icon: React.ComponentProps['name']; } +const CustomBottomNavigation = ({ navigationState, onTabPress, renderIcon, getLabelText, activeColor, inactiveColor, barStyle }: any) => { + const theme = usePaperTheme(); + + return ( + + + {navigationState.routes.map((route: Route, index: number) => { + const focused = navigationState.index === index; + const color = focused ? activeColor : inactiveColor; + + return ( + onTabPress(index)} + style={styles(theme).tabButton} + > + + {renderIcon({ route, focused, color })} + + {getLabelText({ route, focused, color })} + + {focused && ( + + )} + + + ); + })} + + + ); +}; + export default function HomeScreen() { const [index, setIndex] = React.useState(0); - const [routesState] = React.useState(routes); const paperTheme = usePaperTheme(); - const renderScene = ({ route }: { route: Route }) => { - switch (route.key) { - case 'facts': - return ; - case 'combos': - return ; - case 'chat': - return ; - case 'about': - return ; - default: - return null; - } + const navigationState = { + index, + routes: routes.map(route => ({ + ...route, + key: route.key, + })), }; - return ( - ( - - )} - barStyle={[ - styles.barStyle, - { - ...Platform.select({ - android: { elevation: 4 }, - ios: { - shadowColor: '#000', - shadowOffset: { width: 0, height: -3 }, - shadowOpacity: 0.1, - shadowRadius: 3, - }, - }), - }, - ]} - activeColor={paperTheme.colors.primary} - shifting={false} + const renderScene = BottomNavigation.SceneMap({ + facts: FactsRoute, + combos: CombosRoute, + chat: ChatRoute, + about: AboutRoute, + }); + + const renderIcon = ({ route, color }: { route: Route; color: string }) => ( + ); + + const getLabelText = ({ route, color }: { route: Route; color: string }) => ( + {route.title} + ); + + const handleTabPress = (newIndex: number) => { + setIndex(newIndex); + }; + + return ( + + + {renderScene({ route: navigationState.routes[index], jumpTo: () => {} })} + + + + ); } -const styles = StyleSheet.create({ +const styles = (theme: any) => StyleSheet.create({ + bar: { + overflow: 'hidden', + borderTopWidth: 0, + }, icon: { alignSelf: 'center', }, iconContainer: { + flex: 1, justifyContent: 'center', alignItems: 'center', - height: 24, + borderRadius: 16, + paddingVertical: 8, + position: 'relative', }, barStyle: { }, bottomBar: { flexDirection: 'row', - height: 64, - paddingBottom: Platform.OS === 'ios' ? 20 : 8, - paddingTop: 4, + height: 80, + paddingBottom: Platform.OS === 'ios' ? 20 : 12, + paddingTop: 8, + paddingHorizontal: 8, }, tabButton: { flex: 1, - justifyContent: 'center', - alignItems: 'center', borderRadius: 16, - margin: 4, + marginHorizontal: 4, }, - activeDot: { - width: 5, - height: 5, - borderRadius: 2.5, - marginTop: 4, + activeTab: { + backgroundColor: `${theme.colors.primary}12`, }, - inactiveDot: { - width: 5, - height: 5, - marginTop: 4, + activeIndicator: { + position: 'absolute', + bottom: -4, + width: 24, + height: 3, + borderRadius: 2, }, labelContainer: { - marginTop: 2, + marginTop: 4, alignItems: 'center', }, label: { fontSize: 12, textAlign: 'center', + letterSpacing: 0.5, } }); diff --git a/app/themes.tsx b/app/themes.tsx index 40f8527..cc26195 100644 --- a/app/themes.tsx +++ b/app/themes.tsx @@ -69,6 +69,14 @@ export const darkTheme = { inversePrimary: '#6750A4', shadow: '#000000', surfaceTint: '#D0BCFF', + elevation: { + level0: 'transparent', + level1: '#27262a', + level2: '#2d2c30', + level3: '#333236', + level4: '#383739', + level5: '#3d3c3f', + }, }, }; diff --git a/package-lock.json b/package-lock.json index c5579ae..2a9e21e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -17,11 +17,18 @@ "@react-native-async-storage/async-storage": "1.23.1", "@react-native-community/netinfo": "11.4.1", "@react-navigation/native": "^7.0.14", + "@react-navigation/native-stack": "^7.2.1", "@react-navigation/stack": "^7.1.2", + "date-fns": "^4.1.0", "expo": "^52.0.37", + "expo-application": "~6.0.2", "expo-constants": "~17.0.7", + "expo-device": "~7.0.2", + "expo-file-system": "~18.0.11", "expo-font": "~13.0.4", + "expo-insights": "~0.8.2", "expo-linking": "~7.0.5", + "expo-localization": "~16.0.1", "expo-router": "~4.0.17", "expo-splash-screen": "~0.29.22", "expo-status-bar": "~2.0.1", @@ -29,6 +36,7 @@ "expo-updates": "~0.27.1", "expo-web-browser": "~14.0.2", "moment": "^2.30.1", + "posthog-react-native": "^3.11.2", "react": "18.3.1", "react-dom": "18.3.1", "react-native": "0.76.7", @@ -43,6 +51,7 @@ "react-native-vector-icons": "^10.1.0", "react-native-web": "~0.19.10", "react-native-webview": "13.12.5", + "react-navigation-shared-element": "^3.1.3", "victory-native": "^36.8.3" }, "devDependencies": { @@ -4792,6 +4801,23 @@ "react-native": "*" } }, + "node_modules/@react-navigation/native-stack": { + "version": "7.2.1", + "resolved": "https://registry.npmjs.org/@react-navigation/native-stack/-/native-stack-7.2.1.tgz", + "integrity": "sha512-zqC6DVpO4pFZrl+8JuIgV8qk+AGdTuv+hJ5EHePmzs9gYSUrDpw6LahFCiXshwBvi9LinIw9Do7mtnQK2Q8AGA==", + "license": "MIT", + "dependencies": { + "@react-navigation/elements": "^2.2.6", + "warn-once": "^0.1.1" + }, + "peerDependencies": { + "@react-navigation/native": "^7.0.15", + "react": ">= 18.2.0", + "react-native": "*", + "react-native-safe-area-context": ">= 4.0.0", + "react-native-screens": ">= 4.0.0" + } + }, "node_modules/@react-navigation/routers": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/@react-navigation/routers/-/routers-7.2.0.tgz", @@ -8172,6 +8198,16 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/date-fns": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-4.1.0.tgz", + "integrity": "sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/kossnocorp" + } + }, "node_modules/debug": { "version": "4.4.0", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", @@ -9787,6 +9823,15 @@ } } }, + "node_modules/expo-application": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/expo-application/-/expo-application-6.0.2.tgz", + "integrity": "sha512-qcj6kGq3mc7x5yIb5KxESurFTJCoEKwNEL34RdPEvTB/xhl7SeVZlu05sZBqxB1V4Ryzq/LsCb7NHNfBbb3L7A==", + "license": "MIT", + "peerDependencies": { + "expo": "*" + } + }, "node_modules/expo-asset": { "version": "11.0.4", "resolved": "https://registry.npmjs.org/expo-asset/-/expo-asset-11.0.4.tgz", @@ -9818,6 +9863,44 @@ "react-native": "*" } }, + "node_modules/expo-device": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/expo-device/-/expo-device-7.0.2.tgz", + "integrity": "sha512-0PkTixE4Qi8VQBjixnj4aw2f6vE4tUZH7GK8zHROGKlBypZKcWmsA+W/Vp3RC5AyREjX71pO/hjKTSo/vF0E2w==", + "license": "MIT", + "dependencies": { + "ua-parser-js": "^0.7.33" + }, + "peerDependencies": { + "expo": "*" + } + }, + "node_modules/expo-device/node_modules/ua-parser-js": { + "version": "0.7.40", + "resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-0.7.40.tgz", + "integrity": "sha512-us1E3K+3jJppDBa3Tl0L3MOJiGhe1C6P0+nIvQAFYbxlMAx0h81eOwLmU57xgqToduDDPx3y5QsdjPfDu+FgOQ==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/ua-parser-js" + }, + { + "type": "paypal", + "url": "https://paypal.me/faisalman" + }, + { + "type": "github", + "url": "https://github.com/sponsors/faisalman" + } + ], + "license": "MIT", + "bin": { + "ua-parser-js": "script/cli.js" + }, + "engines": { + "node": "*" + } + }, "node_modules/expo-eas-client": { "version": "0.13.3", "resolved": "https://registry.npmjs.org/expo-eas-client/-/expo-eas-client-0.13.3.tgz", @@ -9850,6 +9933,18 @@ "react": "*" } }, + "node_modules/expo-insights": { + "version": "0.8.2", + "resolved": "https://registry.npmjs.org/expo-insights/-/expo-insights-0.8.2.tgz", + "integrity": "sha512-b3rX7WVtvg3/Ejv/z19dGaY9gdq/+Vyx0D6MWVCi5ERY7VD/Yk7O0ubNG4zaxmZXwDxL4tmI/rX10VQ4Gmqq+g==", + "license": "MIT", + "dependencies": { + "expo-eas-client": "~0.13.2" + }, + "peerDependencies": { + "expo": "*" + } + }, "node_modules/expo-json-utils": { "version": "0.14.0", "resolved": "https://registry.npmjs.org/expo-json-utils/-/expo-json-utils-0.14.0.tgz", @@ -9880,6 +9975,19 @@ "react-native": "*" } }, + "node_modules/expo-localization": { + "version": "16.0.1", + "resolved": "https://registry.npmjs.org/expo-localization/-/expo-localization-16.0.1.tgz", + "integrity": "sha512-kUrXiV/Pq9r7cG+TMt+Qa49IUQ9Y/czVwen4hmiboTclTopcWdIeCzYZv6JGtufoPpjEO9vVx1QJrXYl9V2u0Q==", + "license": "MIT", + "dependencies": { + "rtl-detect": "^1.0.2" + }, + "peerDependencies": { + "expo": "*", + "react": "*" + } + }, "node_modules/expo-manifests": { "version": "0.15.7", "resolved": "https://registry.npmjs.org/expo-manifests/-/expo-manifests-0.15.7.tgz", @@ -10015,23 +10123,6 @@ "react-native-screens": ">= 4.0.0" } }, - "node_modules/expo-router/node_modules/@react-navigation/native-stack": { - "version": "7.2.1", - "resolved": "https://registry.npmjs.org/@react-navigation/native-stack/-/native-stack-7.2.1.tgz", - "integrity": "sha512-zqC6DVpO4pFZrl+8JuIgV8qk+AGdTuv+hJ5EHePmzs9gYSUrDpw6LahFCiXshwBvi9LinIw9Do7mtnQK2Q8AGA==", - "license": "MIT", - "dependencies": { - "@react-navigation/elements": "^2.2.6", - "warn-once": "^0.1.1" - }, - "peerDependencies": { - "@react-navigation/native": "^7.0.15", - "react": ">= 18.2.0", - "react-native": "*", - "react-native-safe-area-context": ">= 4.0.0", - "react-native-screens": ">= 4.0.0" - } - }, "node_modules/expo-router/node_modules/semver": { "version": "7.6.3", "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", @@ -15453,6 +15544,51 @@ "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", "license": "MIT" }, + "node_modules/posthog-react-native": { + "version": "3.11.2", + "resolved": "https://registry.npmjs.org/posthog-react-native/-/posthog-react-native-3.11.2.tgz", + "integrity": "sha512-+KAnZeuvdHYr3M5D+D2kcKIYf/V+Qt6KKbztnq4Jz+rgY/ANN/hzTc6Xz63BB0gZBqbssnaPCWbeO4Y5PJlKVQ==", + "peerDependencies": { + "@react-native-async-storage/async-storage": ">=1.0.0", + "@react-navigation/native": ">= 5.0.10", + "expo-application": ">= 4.0.0", + "expo-device": ">= 4.0.0", + "expo-file-system": ">= 13.0.0", + "expo-localization": ">= 11.0.0", + "posthog-react-native-session-replay": "^1.0.0", + "react-native-device-info": ">= 10.0.0", + "react-native-navigation": ">=6.0.0" + }, + "peerDependenciesMeta": { + "@react-native-async-storage/async-storage": { + "optional": true + }, + "@react-navigation/native": { + "optional": true + }, + "expo-application": { + "optional": true + }, + "expo-device": { + "optional": true + }, + "expo-file-system": { + "optional": true + }, + "expo-localization": { + "optional": true + }, + "posthog-react-native-session-replay": { + "optional": true + }, + "react-native-device-info": { + "optional": true + }, + "react-native-navigation": { + "optional": true + } + } + }, "node_modules/prelude-ls": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", @@ -16041,6 +16177,13 @@ "react-native": "*" } }, + "node_modules/react-native-shared-element": { + "version": "0.8.9", + "resolved": "https://registry.npmjs.org/react-native-shared-element/-/react-native-shared-element-0.8.9.tgz", + "integrity": "sha512-vlzhv3amkJm+8gA0WSeLzcCKNtN/ypZbic3IZ4Bwwr6GeWDrYzZ6k7PdHCioy7fwIVOJ1X9Pi/aYF9HK4Kb0qg==", + "license": "MIT", + "peer": true + }, "node_modules/react-native-svg": { "version": "15.8.0", "resolved": "https://registry.npmjs.org/react-native-svg/-/react-native-svg-15.8.0.tgz", @@ -16242,6 +16385,20 @@ "async-limiter": "~1.0.0" } }, + "node_modules/react-navigation-shared-element": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/react-navigation-shared-element/-/react-navigation-shared-element-3.1.3.tgz", + "integrity": "sha512-U1BZp7dEdcTNHggfkq3WEBlJeg4HwFhFdj7a0i0Uql/7mg2IHQg/bZaqM2jQvJITkABge6Hz5fZixIF8jyzpkg==", + "license": "MIT", + "dependencies": { + "hoist-non-react-statics": "^3.3.2" + }, + "peerDependencies": { + "react": "*", + "react-native": "*", + "react-native-shared-element": "*" + } + }, "node_modules/react-refresh": { "version": "0.14.2", "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.14.2.tgz", @@ -16623,6 +16780,12 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/rtl-detect": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/rtl-detect/-/rtl-detect-1.1.2.tgz", + "integrity": "sha512-PGMBq03+TTG/p/cRB7HCLKJ1MgDIi07+QU1faSjiYRfmY5UsAttV9Hs08jDAHVwcOwmVLcSJkpwyfXszVjWfIQ==", + "license": "BSD-3-Clause" + }, "node_modules/run-parallel": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", diff --git a/package.json b/package.json index ad29c62..5b40a9a 100644 --- a/package.json +++ b/package.json @@ -25,11 +25,18 @@ "@react-native-async-storage/async-storage": "1.23.1", "@react-native-community/netinfo": "11.4.1", "@react-navigation/native": "^7.0.14", + "@react-navigation/native-stack": "^7.2.1", "@react-navigation/stack": "^7.1.2", + "date-fns": "^4.1.0", "expo": "^52.0.37", + "expo-application": "~6.0.2", "expo-constants": "~17.0.7", + "expo-device": "~7.0.2", + "expo-file-system": "~18.0.11", "expo-font": "~13.0.4", + "expo-insights": "~0.8.2", "expo-linking": "~7.0.5", + "expo-localization": "~16.0.1", "expo-router": "~4.0.17", "expo-splash-screen": "~0.29.22", "expo-status-bar": "~2.0.1", @@ -37,6 +44,7 @@ "expo-updates": "~0.27.1", "expo-web-browser": "~14.0.2", "moment": "^2.30.1", + "posthog-react-native": "^3.11.2", "react": "18.3.1", "react-dom": "18.3.1", "react-native": "0.76.7", @@ -51,6 +59,7 @@ "react-native-vector-icons": "^10.1.0", "react-native-web": "~0.19.10", "react-native-webview": "13.12.5", + "react-navigation-shared-element": "^3.1.3", "victory-native": "^36.8.3" }, "devDependencies": {