diff --git a/.env.example b/.env.example index 6cac7d35..2e2bf05b 100644 --- a/.env.example +++ b/.env.example @@ -1,2 +1,2 @@ API_BASE_PATH=https://app.didattica.polito.it/mock/api -MAPBOX_TOKEN= \ No newline at end of file +MAPBOX_TOKEN= diff --git a/.eslintrc b/.eslintrc index c635590d..6b7c2205 100644 --- a/.eslintrc +++ b/.eslintrc @@ -16,7 +16,7 @@ "rules": { "react-native/split-platform-components": 2, "react-native/no-color-literals": 1, - "react-native/no-raw-text": 2, + "react-native/no-raw-text": 0, "react-native/no-single-element-style-arrays": 2 }, "overrides": [ diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index b4b7b283..d4f9786d 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -43,7 +43,6 @@ jobs: outputs: version: ${{ steps.configure.outputs.version }} build_no: ${{ steps.configure.outputs.build_no }} - build_piece: ${{ steps.configure.outputs.build_piece }} push_to_testflight: ${{ steps.configure.outputs.push_to_testflight }} push_to_playstore: ${{ steps.configure.outputs.push_to_playstore }} steps: @@ -112,12 +111,11 @@ jobs: BUILD_NO=$(( MAJOR*1000000 + MINOR*10000 + SUBV*100 + BUILD_PIECE )) echo "build_no=$BUILD_NO" >> $GITHUB_OUTPUT echo "version=$MAJOR.$MINOR.$SUBV" >> $GITHUB_OUTPUT - echo "build_piece=$BUILD_PIECE" >> $GITHUB_OUTPUT elif [[ "$VERSION" != "" ]]; then echo "INVALID VERSION FORMAT: $VERSION" exit 2 fi - + echo "push_to_playstore=$PUSH_PLAYSTORE" >> $GITHUB_OUTPUT echo "push_to_testflight=$PUSH_TESTFLIGHT" >> $GITHUB_OUTPUT echo "CONFIGURED:" @@ -132,18 +130,7 @@ jobs: run: printf '@polito:registry=https://npm.pkg.github.com\n//npm.pkg.github.com/:_authToken=${{ secrets.GITHUB_TOKEN }}\n' > .npmrc - name: Install npm dependencies run: npm install - - name: Sentry Release - uses: getsentry/action-release@v1.4.1 - if: steps.configure.outputs.build_no != '' - env: - SENTRY_URL: ${{ vars.SENTRY_URL }} - SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }} - SENTRY_PROJECT: ${{ vars.SENTRY_PROJECT }} - SENTRY_ORG: ${{ vars.SENTRY_ORG }} - with: - environment: 'production' - ignore_missing: true - version: it.polito.students@${{ steps.configure.outputs.version }}+${{ steps.configure.outputs.build_piece }} + build-android: name: ${{ needs.configure.outputs.push_to_playstore == 'false' && 'Build' || 'Build & Deploy' }} Android @@ -153,7 +140,6 @@ jobs: LANG: en_US.UTF-8 CI: 'true' BUILD_NO: ${{ needs.configure.outputs.build_no }} - BUILD_PIECE: ${{ needs.configure.outputs.build_piece }} APP_VERSION: ${{ needs.configure.outputs.version }} SUPPLY_GOOGLE_JSON_PATH: './pc-api-secret.json' KEYSTORE_PW: ${{ secrets.ANDROID_KEYSTORE_PW }} @@ -163,8 +149,10 @@ jobs: uses: actions/checkout@v3 - name: Set .env run: - echo "APP_VERSION=${{ needs.configure.outputs.version }}" >> .env - echo "APP_BUILD=${{ needs.configure.outputs.build_piece }}" >> .env + echo "MAPBOX_TOKEN=${{ secrets.MAPBOX_TOKEN }}" >> .env + - name: Set local.properties + run: + echo "MAPBOX_DOWNLOADS_TOKEN=${{ secrets.MAPBOX_TOKEN }}" >> android/local.properties - name: Gradle Wrapper Validation uses: gradle/wrapper-validation-action@v1 - name: Setup Ruby @@ -235,7 +223,6 @@ jobs: LANG: en_US.UTF-8 CI: 'true' BUILD_NO: ${{ needs.configure.outputs.build_no }} - BUILD_PIECE: ${{ needs.configure.outputs.build_piece }} APP_VERSION: ${{ needs.configure.outputs.version }} MATCH_PASSWORD: ${{ secrets.FASTLANE_MATCH_KEY }} MATCH_GIT_PRIVATE_KEY: "./github_access.pk" @@ -253,8 +240,7 @@ jobs: uses: actions/checkout@v3 - name: Set .env run: - echo "APP_VERSION=${{ needs.configure.outputs.version }}" >> .env - echo "APP_BUILD=${{ needs.configure.outputs.build_piece }}" >> .env + echo "MAPBOX_TOKEN=${{ secrets.MAPBOX_TOKEN }}" >> .env - name: Setup Ruby uses: ruby/setup-ruby@v1 with: @@ -326,4 +312,3 @@ jobs: generateReleaseNotes: true skipIfReleaseExists: true artifacts: ./android-release/*.apk - tag: v1.4.0 diff --git a/.prettierrc b/.prettierrc index 529c1450..c185f692 100644 --- a/.prettierrc +++ b/.prettierrc @@ -5,7 +5,7 @@ "bracketSpacing": true, "endOfLine": "auto", "semi": true, - "importOrder": ["^react", "^@(?!ui/)(.*)$", "^\\w", "^@ui/(.*)$", "^[./]"], + "importOrder": ["react-native-gesture-handler", "^react", "^@(?!ui/)(.*)$", "^\\w", "^@ui/(.*)$", "^[./]"], "importOrderSeparation": true, "importOrderSortSpecifiers": true } diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 2d6c0645..cebf4254 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -11,7 +11,7 @@ the [gallium](https://nodejs.org/download/release/v16.16.0/) LTS release). [Nvm](https://github.com/nvm-sh/nvm) can be used to automatically select the correct version enforced by [.nvmrc](./.nvmrc), see [Deeper Shell integration](https://github.com/nvm-sh/nvm#deeper-shell-integration). -### Adding a GitHub personal access token to pull from package repositories +### Add a GitHub Personal Access Token GitHub requires the usage of a personal access token to pull from public registries. @@ -31,6 +31,21 @@ Windows) with the following content: //npm.pkg.github.com/:_authToken=YOUR_TOKEN_HERE ``` +### Add a MapBox token + +MapBox requires a token in order to install the `@rnmapbox/maps` npm package and pull native dependencies. Create a +MapBox account and [create a new token](https://account.mapbox.com/access-tokens/create) with all the public scopes and +the `DOWNLOADS:READ` scope. Paste the generated token in the following files: + +- `.env` +- `android/local.properties` +- a `.netrc` file in your home directory with the following content: + ``` + machine api.mapbox.com + login mapbox + password + ``` + ## Project setup ```shell diff --git a/android/build.gradle b/android/build.gradle index 3b1ea24b..9e8b8a15 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -7,16 +7,42 @@ buildscript { minSdkVersion = 21 compileSdkVersion = 33 targetSdkVersion = 33 + RNMapboxMapsImpl = "mapbox" // We use NDK 23 which has both M1 support and is the side-by-side NDK version from AGP. ndkVersion = "23.1.7779620" } + repositories { google() mavenCentral() } + dependencies { classpath("com.android.tools.build:gradle:7.4.2") classpath("com.facebook.react:react-native-gradle-plugin") } } + + +def localPropertiesFile = rootProject.file("local.properties") +def localProperties = new Properties() +localProperties.load(new FileInputStream(localPropertiesFile)) + +allprojects { + repositories { + maven { + url 'https://api.mapbox.com/downloads/v2/releases/maven' + authentication { + basic(BasicAuthentication) + } + credentials { + // Do not change the username below. + // This should always be `mapbox` (not your username). + username = 'mapbox' + // Use the secret token you stored in gradle.properties as the password + password = localProperties['MAPBOX_DOWNLOADS_TOKEN'] ?: "" + } + } + } +} diff --git a/assets/map-icons/bar.png b/assets/map-icons/bar.png new file mode 100644 index 00000000..e1df00c8 Binary files /dev/null and b/assets/map-icons/bar.png differ diff --git a/assets/map-icons/bed.png b/assets/map-icons/bed.png new file mode 100644 index 00000000..4b9b5b46 Binary files /dev/null and b/assets/map-icons/bed.png differ diff --git a/assets/map-icons/bike.png b/assets/map-icons/bike.png new file mode 100644 index 00000000..cf87e7e4 Binary files /dev/null and b/assets/map-icons/bike.png differ diff --git a/assets/map-icons/car.png b/assets/map-icons/car.png new file mode 100644 index 00000000..dd0a03f8 Binary files /dev/null and b/assets/map-icons/car.png differ diff --git a/assets/map-icons/classroom.png b/assets/map-icons/classroom.png new file mode 100644 index 00000000..82d984cc Binary files /dev/null and b/assets/map-icons/classroom.png differ diff --git a/assets/map-icons/conference.png b/assets/map-icons/conference.png new file mode 100644 index 00000000..334829e8 Binary files /dev/null and b/assets/map-icons/conference.png differ diff --git a/assets/map-icons/door.png b/assets/map-icons/door.png new file mode 100644 index 00000000..f6bb622a Binary files /dev/null and b/assets/map-icons/door.png differ diff --git a/assets/map-icons/lab.png b/assets/map-icons/lab.png new file mode 100644 index 00000000..a82d2ffe Binary files /dev/null and b/assets/map-icons/lab.png differ diff --git a/assets/map-icons/library.png b/assets/map-icons/library.png new file mode 100644 index 00000000..c0a8bad4 Binary files /dev/null and b/assets/map-icons/library.png differ diff --git a/assets/map-icons/medical.png b/assets/map-icons/medical.png new file mode 100644 index 00000000..024687ec Binary files /dev/null and b/assets/map-icons/medical.png differ diff --git a/assets/map-icons/office.png b/assets/map-icons/office.png new file mode 100644 index 00000000..285a991d Binary files /dev/null and b/assets/map-icons/office.png differ diff --git a/assets/map-icons/pin.png b/assets/map-icons/pin.png new file mode 100644 index 00000000..f727ae06 Binary files /dev/null and b/assets/map-icons/pin.png differ diff --git a/assets/map-icons/post.png b/assets/map-icons/post.png new file mode 100644 index 00000000..34251bad Binary files /dev/null and b/assets/map-icons/post.png differ diff --git a/assets/map-icons/print.png b/assets/map-icons/print.png new file mode 100644 index 00000000..419b08ab Binary files /dev/null and b/assets/map-icons/print.png differ diff --git a/assets/map-icons/restaurant.png b/assets/map-icons/restaurant.png new file mode 100644 index 00000000..4045d938 Binary files /dev/null and b/assets/map-icons/restaurant.png differ diff --git a/assets/map-icons/restroom.png b/assets/map-icons/restroom.png new file mode 100644 index 00000000..ac089722 Binary files /dev/null and b/assets/map-icons/restroom.png differ diff --git a/assets/map-icons/service.png b/assets/map-icons/service.png new file mode 100644 index 00000000..f2b7df53 Binary files /dev/null and b/assets/map-icons/service.png differ diff --git a/assets/map-icons/stairs.png b/assets/map-icons/stairs.png new file mode 100644 index 00000000..83354928 Binary files /dev/null and b/assets/map-icons/stairs.png differ diff --git a/assets/map-icons/study.png b/assets/map-icons/study.png new file mode 100644 index 00000000..51d228ab Binary files /dev/null and b/assets/map-icons/study.png differ diff --git a/assets/map-icons/water.png b/assets/map-icons/water.png new file mode 100644 index 00000000..0e497f72 Binary files /dev/null and b/assets/map-icons/water.png differ diff --git a/assets/translations/en.json b/assets/translations/en.json index 4c4edcc4..2e4c2e2e 100644 --- a/assets/translations/en.json +++ b/assets/translations/en.json @@ -60,10 +60,13 @@ "areYouSure?": "Are you sure?", "booking": "Booking", "booking_plural": "Bookings", + "building": "Building", "cache": "Cache", "cacheMiss": "Come back online to view", "camera": "Camera", + "campus": "Campus", "cancel": "Cancel", + "capacity": "Capacity", "classroomsNotSpecified": "Classroom not specified", "cleanCourseFiles": "Remove all course files", "close": "Close", @@ -116,12 +119,16 @@ "unavailable": "Unavailable" }, "externalLink": "External link. By pressing the link you will be redirected to an external browser", + "facilities": "Facilities", "file": "File", "file_plural": "Files", + "floor": "Floor", "follow": "Follow", + "free": "Free", "grade": "Grade", "hours": "Hour", "icon": "Icon", + "inYourAgenda": "in your agenda", "it": "Italian", "language": "Language", "lecture": "Lecture", @@ -129,9 +136,11 @@ "loading": "Loading", "logout": "Logout", "minutes": "Minutes", + "navigate": "Navigate", "newItems": "New item", "newItems_plural": "New items", "noInternet": "You are offline, some features may not be available", + "notFound": "Not found", "notice": "Notice", "notice_plural": "Notices", "notifications": "Notifications", @@ -156,6 +165,7 @@ "play": "Play", "preferences": "Preferences", "profilePic": "Profile picture", + "recentlyViewed": "Recently viewed", "refresh": "Refresh", "remove": "Remove", "retract": "Retract", @@ -169,6 +179,7 @@ "status": "Status", "stop": "Stop", "stopFollowing": "Stop following", + "structure": "Structure", "switchCareerLabel": "Tap to switch career", "teacher": "Teacher", "theme": "Theme", @@ -179,6 +190,7 @@ "tomorrow": "Tomorrow", "transcript": "Transcript", "unnamedFile": "Unnamed file", + "untitled": "Untitled", "updatedAt": "Updated at", "upload": "Upload", "username": "Student ID", @@ -337,6 +349,9 @@ "title": "Exam calls", "total": "There are {{ total }} exam calls" }, + "freeRoomsScreen": { + "title": "Free rooms" + }, "grades": { "absent": "Abs", "fail": "Fail", @@ -434,8 +449,29 @@ "sentEmail": "Send e-mail", "totalCourses": "There are {{total}} courses" }, + "placeCategories": { + "classrooms": "Classrooms", + "libraries": "Libraries", + "studyRooms": "Study rooms" + }, + "placeScreen": { + "capacity": "{{count}} seat", + "capacity_plural": "{{count}} seats", + "placeNotFound": "Place not found", + "title": "Place" + }, "placesScreen": { - "title": "Places" + "changeFloor": "Change floor", + "goToMyPosition": "Go to my position", + "multipleFloors": "Results are on multiple floors", + "noCategoriesFound": "No categories found", + "noPlacesFound": "No places found", + "searchCategories": "Search categories", + "title": "Places", + "viewWholeCampus": "View the whole campus" + }, + "placesSearchScreen": { + "searchPlaceholder": "Try with \"Room 1\"" }, "profileScreen": { "careerStatus": "Career status:", diff --git a/assets/translations/it.json b/assets/translations/it.json index ce25e1bf..bb430bab 100644 --- a/assets/translations/it.json +++ b/assets/translations/it.json @@ -60,10 +60,13 @@ "areYouSure?": "Sei sicuro?", "booking": "Prenotazione", "booking_plural": "Prenotazioni", + "building": "Edificio", "cache": "Cache", "cacheMiss": "Torna online per visualizzare", "camera": "Fotocamera", + "campus": "Sede", "cancel": "Annulla", + "capacity": "Capacità", "classroomsNotSpecified": "Luogo non specificato", "cleanCourseFiles": "Rimuovi file dei corsi", "close": "Chiudi", @@ -116,12 +119,16 @@ "unavailable": "Non prenotabile" }, "externalLink": "Link esterno. Premendo il link verrai reindirizzato su un browser esterno", + "facilities": "Dotazioni", "file": "File", "file_plural": "File", + "floor": "Piano", "follow": "Segui", + "free": "Libera", "grade": "Voto", "hours": "Ore", "icon": "Icona", + "inYourAgenda": "nella tua agenda", "it": "Italiano", "language": "Lingua", "lecture": "Lezione", @@ -129,9 +136,11 @@ "loading": "Caricamento", "logout": "Logout", "minutes": "Minuti", + "navigate": "Naviga", "newItems": "Nuovo elemento", "newItems_plural": "Nuovi elementi", "noInternet": "Sei offline, alcune funzionalità potrebbero non essere disponibili", + "notFound": "Luogo inesistente", "notice": "Avviso", "notice_plural": "Avvisi", "notifications": "Notifiche", @@ -156,6 +165,7 @@ "play": "Riproduci", "preferences": "Preferenze", "profilePic": "Immagine di profilo", + "recentlyViewed": "Visto di recente", "refresh": "Aggiorna", "remove": "Rimuovi", "retract": "Ritira", @@ -169,7 +179,8 @@ "status": "Stato", "stop": "Stop", "stopFollowing": "Non seguire", - "switchCareerLabel": "Premi per affettuare un cambio matricola", + "structure": "Struttura", + "switchCareerLabel": "Premi per effettuare un cambio matricola", "teacher": "Docente", "theme": "Tema", "ticket_plural": "Ticket", @@ -179,6 +190,7 @@ "tomorrow": "Domani", "transcript": "Libretto", "unnamedFile": "File senza nome", + "untitled": "Senza titolo", "updatedAt": "Aggiornato il", "upload": "Carica", "username": "Matricola", @@ -337,6 +349,9 @@ "title": "Appelli", "total": "Sono presenti {{ total }} appelli" }, + "freeRoomsScreen": { + "title": "Aule libere" + }, "grades": { "absent": "Ass", "fail": "Ins", @@ -403,15 +418,15 @@ }, "messagingView": { "pickFile": "Scegli un file", - "pickFileHint": "Dal tuo dispositivio o dal cloud", + "pickFileHint": "Dal tuo dispositivo o dal cloud", "pickPhoto": "Scegli un'immagine", "pickPhotoHint": "Dalla tua galleria", - "takePhoto": "Acquisci con la fotocamera", + "takePhoto": "Acquisisci con la fotocamera", "takePhotoHint": "Scatta una o più foto e genera un file" }, "newsScreen": { "createdAt": "Pubblicato il ", - "eventStartTime": "Inicio evento", + "eventStartTime": "Inizio evento", "information": "Informazioni", "logo": "Logo", "title": "Avvisi & News" @@ -434,8 +449,29 @@ "sentEmail": "Invia e-mail", "totalCourses": "Sono presenti {{total}} corsi" }, + "placeCategories": { + "classrooms": "Aule", + "libraries": "Biblioteche", + "studyRooms": "Aule studio" + }, + "placeScreen": { + "capacity": "{{count}} posto", + "capacity_plural": "{{count}} posti", + "placeNotFound": "Questo luogo non esiste", + "title": "Luogo" + }, "placesScreen": { - "title": "Luoghi" + "changeFloor": "Cambia piano", + "goToMyPosition": "Vai alla mia posizione", + "multipleFloors": "I risultati sono su più piani", + "noCategoriesFound": "Nessuna categoria trovata", + "noPlacesFound": "Nessun posto trovato", + "searchCategories": "Cerca categorie", + "title": "Luoghi", + "viewWholeCampus": "Vedi tutta la sede" + }, + "placesSearchScreen": { + "searchPlaceholder": "Prova con \"Aula 1\"" }, "profileScreen": { "careerStatus": "Stato carriera:", @@ -485,7 +521,7 @@ "writeTicket": "Scrivi ticket" }, "ticketFaqsScreen": { - "emptyState": "Nessun risutato trovato", + "emptyState": "Nessun risultato trovato", "findFAQ": "Cerca tra le FAQ", "findFAQSubtitle": "Prima di inserire un ticket cerca una risposta tra le FAQ.", "noResultFound": "Non hai trovato quello che cerchi?", diff --git a/babel.config.js b/babel.config.js index e01a4976..9c1591eb 100644 --- a/babel.config.js +++ b/babel.config.js @@ -12,7 +12,8 @@ module.exports = function (api) { }, }, ], - ["module:react-native-dotenv"] + 'module:react-native-dotenv', + 'react-native-reanimated/plugin', ], }; }; diff --git a/index.js b/index.js index 2ffc3174..e054c66b 100644 --- a/index.js +++ b/index.js @@ -1,3 +1,5 @@ +import 'react-native-gesture-handler'; + import { AppRegistry } from 'react-native'; import * as Sentry from '@sentry/react-native'; diff --git a/ios/Podfile b/ios/Podfile index e67f7dfc..f51914da 100644 --- a/ios/Podfile +++ b/ios/Podfile @@ -13,6 +13,7 @@ prepare_react_native_project! # dependencies: { # ...(process.env.NO_FLIPPER ? { 'react-native-flipper': { platforms: { ios: null } } } : {}), # ``` +$RNMapboxMapsImpl = 'mapbox' flipper_config = ENV['NO_FLIPPER'] == "1" ? FlipperConfiguration.disabled : FlipperConfiguration.enabled(["Debug"], { 'Flipper' => '0.189.0' }) linkage = ENV['USE_FRAMEWORKS'] if linkage != nil @@ -82,5 +83,11 @@ target 'students' do end end end + + $RNMapboxMaps.post_install(installer) end end + +pre_install do |installer| + $RNMapboxMaps.pre_install(installer) +end diff --git a/ios/Podfile.lock b/ios/Podfile.lock index 5a2f1a2a..00c66411 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -118,6 +118,15 @@ PODS: - libwebp/mux (1.2.4): - libwebp/demux - libwebp/webp (1.2.4) + - MapboxCommon (23.6.0) + - MapboxCoreMaps (10.14.0): + - MapboxCommon (~> 23.6) + - MapboxMaps (10.14.0): + - MapboxCommon (= 23.6.0) + - MapboxCoreMaps (= 10.14.0) + - MapboxMobileEvents (= 1.0.10) + - Turf (~> 2.0) + - MapboxMobileEvents (1.0.10) - OpenSSL-Universal (1.1.1100) - PromisesObjC (2.2.0) - RCT-Folly (2021.07.22.00): @@ -381,7 +390,7 @@ PODS: - React-Core - react-native-html-to-pdf (0.12.0): - React-Core - - react-native-menu (0.7.3): + - react-native-menu (0.8.0): - React - react-native-netinfo (9.3.4): - React-Core @@ -500,7 +509,7 @@ PODS: - React-Core - RNFS (2.20.0): - React-Core - - RNGestureHandler (2.9.0): + - RNGestureHandler (2.13.1): - React-Core - RNImageCropPicker (0.38.0): - React-Core @@ -513,12 +522,52 @@ PODS: - TOCropViewController - RNKeychain (8.1.1): - React-Core + - rnmapbox-maps (10.0.10): + - MapboxMaps (~> 10.14.0) + - React + - React-Core + - rnmapbox-maps/DynamicLibrary (= 10.0.10) + - Turf + - rnmapbox-maps/DynamicLibrary (10.0.10): + - MapboxMaps (~> 10.14.0) + - React + - React-Core + - Turf + - RNPermissions (3.8.4): + - React-Core + - RNReanimated (2.17.0): + - DoubleConversion + - FBLazyVector + - FBReactNativeSpec + - glog + - RCT-Folly + - RCTRequired + - RCTTypeSafety + - React-callinvoker + - React-Core + - React-Core/DevSupport + - React-Core/RCTWebSocket + - React-CoreModules + - React-cxxreact + - React-jsi + - React-jsiexecutor + - React-jsinspector + - React-RCTActionSheet + - React-RCTAnimation + - React-RCTBlob + - React-RCTImage + - React-RCTLinking + - React-RCTNetwork + - React-RCTSettings + - React-RCTText + - ReactCommon/turbomodule/core + - Yoga - RNScreens (3.22.0): - React-Core - React-RCTImage - - RNSentry (5.9.1): + - RNSentry (5.10.0): - React-Core - - Sentry/HybridSDK (= 8.10.0) + - Sentry/HybridSDK (= 8.11.0) - RNSVG (13.4.0): - React-Core - SDWebImage (5.11.1): @@ -527,11 +576,12 @@ PODS: - SDWebImageWebPCoder (0.8.5): - libwebp (~> 1.0) - SDWebImage/Core (~> 5.10) - - Sentry/HybridSDK (8.10.0): - - SentryPrivate (= 8.10.0) - - SentryPrivate (8.10.0) + - Sentry/HybridSDK (8.11.0): + - SentryPrivate (= 8.11.0) + - SentryPrivate (8.11.0) - SocketRocket (0.6.0) - TOCropViewController (2.6.1) + - Turf (2.6.1) - Yoga (1.14.0) - YogaKit (1.18.1): - Yoga (~> 1.14) @@ -617,6 +667,9 @@ DEPENDENCIES: - RNGestureHandler (from `../node_modules/react-native-gesture-handler`) - RNImageCropPicker (from `../node_modules/react-native-image-crop-picker`) - RNKeychain (from `../node_modules/react-native-keychain`) + - "rnmapbox-maps (from `../node_modules/@rnmapbox/maps`)" + - RNPermissions (from `../node_modules/react-native-permissions`) + - RNReanimated (from `../node_modules/react-native-reanimated`) - RNScreens (from `../node_modules/react-native-screens`) - "RNSentry (from `../node_modules/@sentry/react-native`)" - RNSVG (from `../node_modules/react-native-svg`) @@ -638,6 +691,10 @@ SPEC REPOS: - GoogleUtilities - libevent - libwebp + - MapboxCommon + - MapboxCoreMaps + - MapboxMaps + - MapboxMobileEvents - OpenSSL-Universal - PromisesObjC - SDWebImage @@ -646,6 +703,7 @@ SPEC REPOS: - SentryPrivate - SocketRocket - TOCropViewController + - Turf - YogaKit EXTERNAL SOURCES: @@ -757,6 +815,12 @@ EXTERNAL SOURCES: :path: "../node_modules/react-native-image-crop-picker" RNKeychain: :path: "../node_modules/react-native-keychain" + rnmapbox-maps: + :path: "../node_modules/@rnmapbox/maps" + RNPermissions: + :path: "../node_modules/react-native-permissions" + RNReanimated: + :path: "../node_modules/react-native-reanimated" RNScreens: :path: "../node_modules/react-native-screens" RNSentry: @@ -787,6 +851,10 @@ SPEC CHECKSUMS: hermes-engine: 6351580c827b3b03e5f25aadcf989f582d0b0a86 libevent: 4049cae6c81cdb3654a443be001fb9bdceff7913 libwebp: f62cb61d0a484ba548448a4bd52aabf150ff6eef + MapboxCommon: 4a0251dd470ee37e7fadda8e285c01921a5e1eb0 + MapboxCoreMaps: eb07203bbb0b1509395db5ab89cd3ad6c2e3c04c + MapboxMaps: af50ec61a7eb3b032c3f7962c6bd671d93d2a209 + MapboxMobileEvents: de50b3a4de180dd129c326e09cd12c8adaaa46d6 OpenSSL-Universal: ebc357f1e6bc71fa463ccb2fe676756aff50e88c PromisesObjC: 09985d6d70fbe7878040aa746d78236e6946d2ef RCT-Folly: 424b8c9a7a0b9ab2886ffe9c3b041ef628fd4fb1 @@ -808,7 +876,7 @@ SPEC CHECKSUMS: react-native-document-picker: 958e2bc82e128be69055be261aeac8d872c8d34c react-native-geolocation: ef66fb798d96284c6043f0b16c15d9d1d4955db4 react-native-html-to-pdf: 4c5c6e26819fe202971061594058877aa9b25265 - react-native-menu: 9d7d6f819cc7fa14a15cf86888c53f3240d86f1b + react-native-menu: 5b9b82dabcc779cebde15d292ccbcba4469ff311 react-native-netinfo: 6ac9bcc7c88dc51717bda3484eb99093acb755e0 react-native-override-color-scheme: 3badbbee9a4ea2a9c3e5b4a5d9fd9eaba8761870 react-native-pager-view: 0ccb8bf60e2ebd38b1f3669fa3650ecce81db2df @@ -834,21 +902,25 @@ SPEC CHECKSUMS: RNFastImage: 5c9c9fed9c076e521b3f509fe79e790418a544e8 RNFileViewer: ce7ca3ac370e18554d35d6355cffd7c30437c592 RNFS: 4ac0f0ea233904cb798630b3c077808c06931688 - RNGestureHandler: 071d7a9ad81e8b83fe7663b303d132406a7d8f39 + RNGestureHandler: 38aa38413896620338948fbb5c90579a7b1c3fde RNImageCropPicker: ffbba608264885c241cbf3a8f78eb7aeeb978241 RNKeychain: ff836453cba46938e0e9e4c22e43d43fa2c90333 + rnmapbox-maps: de67407973410cb0159c8cee4544c6ba59f6154a + RNPermissions: 8d1bdae7b50b7e97f2bc7bc49b8502c19d39dd2e + RNReanimated: f186e85d9f28c9383d05ca39e11dd194f59093ec RNScreens: 68fd1060f57dd1023880bf4c05d74784b5392789 - RNSentry: 0a1daa8ee81e2776f977ae8c66e67c8d85587828 + RNSentry: 11917f7bf3e28806aca4c2791c6ba7522d8f09fe RNSVG: 07dbd870b0dcdecc99b3a202fa37c8ca163caec2 SDWebImage: a7f831e1a65eb5e285e3fb046a23fcfbf08e696d SDWebImageWebPCoder: 908b83b6adda48effe7667cd2b7f78c897e5111d - Sentry: 71cd4427146ac56eab6e70401ac7a24384c3d3b5 - SentryPrivate: 9334613897c85a9e30f2c9d7a331af8aaa4fe71f + Sentry: 39d57e691e311bdb73bc1ab5bbebbd6bc890050d + SentryPrivate: 48712023cdfd523735c2edb6b06bedf26c4730a3 SocketRocket: fccef3f9c5cedea1353a9ef6ada904fde10d6608 TOCropViewController: edfd4f25713d56905ad1e0b9f5be3fbe0f59c863 + Turf: 469ce2c3d22e5e8e4818d5a3b254699a5c89efa4 Yoga: 5b0304b3dbef2b52e078052138e23a19c7dacaef YogaKit: f782866e155069a2cca2517aafea43200b01fd5a -PODFILE CHECKSUM: 32f0f2e14a5758866a477e768725138610ebf80e +PODFILE CHECKSUM: fb0acba94ebc4f1b1d3f3096c2f7841e9861796f -COCOAPODS: 1.12.1 +COCOAPODS: 1.11.3 diff --git a/lib/ui/components/BottomSheet.tsx b/lib/ui/components/BottomSheet.tsx new file mode 100644 index 00000000..9e8c8240 --- /dev/null +++ b/lib/ui/components/BottomSheet.tsx @@ -0,0 +1,89 @@ +import { Ref, forwardRef } from 'react'; +import { Platform } from 'react-native'; +import { + Extrapolation, + interpolate, + useAnimatedStyle, + useSharedValue, +} from 'react-native-reanimated'; + +import BaseBottomSheet, { + BottomSheetProps as BaseBottomSheetProps, +} from '@gorhom/bottom-sheet'; +import { BottomSheetMethods } from '@gorhom/bottom-sheet/lib/typescript/types'; +import { useTheme } from '@lib/ui/hooks/useTheme'; + +import { TranslucentView } from '../../../src/core/components/TranslucentView'; +import { IS_ANDROID } from '../../../src/core/constants'; + +export type BottomSheetProps = Omit & { + snapPoints?: BaseBottomSheetProps['snapPoints']; + middleSnapPoint?: number; +}; + +export const BottomSheet = forwardRef( + ( + { + middleSnapPoint = 25, + children, + style, + animatedPosition, + ...props + }: BottomSheetProps, + ref: Ref, + ) => { + const { colors, palettes, shapes, spacing } = useTheme(); + const defaultPosition = useSharedValue(0); + const panelPosition = animatedPosition ?? defaultPosition; + + const cornerStyles = useAnimatedStyle(() => { + const radius = interpolate( + panelPosition.value, + [0, shapes.lg], + [0, shapes.lg], + Extrapolation.CLAMP, + ); + return { + borderTopLeftRadius: radius, + borderTopRightRadius: radius, + }; + }); + + return ( + ( + + )} + animatedPosition={panelPosition} + {...props} + > + {children} + + ); + }, +); diff --git a/lib/ui/components/BottomSheetTextField.tsx b/lib/ui/components/BottomSheetTextField.tsx new file mode 100644 index 00000000..650785d0 --- /dev/null +++ b/lib/ui/components/BottomSheetTextField.tsx @@ -0,0 +1,45 @@ +import { memo, useCallback } from 'react'; +import { NativeSyntheticEvent } from 'react-native'; + +import { useBottomSheetInternal } from '@gorhom/bottom-sheet'; +import { + TranslucentTextField, + TranslucentTextFieldProps, +} from '@lib/ui/components/TranslucentTextField'; + +const BottomSheetTextFieldComponent = ({ + onFocus, + onBlur, + ...rest +}: TranslucentTextFieldProps) => { + const { shouldHandleKeyboardEvents } = useBottomSheetInternal(); + + const handleOnFocus = useCallback( + (args: NativeSyntheticEvent) => { + shouldHandleKeyboardEvents.value = true; + if (onFocus) { + onFocus(args); + } + }, + [onFocus, shouldHandleKeyboardEvents], + ); + const handleOnBlur = useCallback( + (args: NativeSyntheticEvent) => { + shouldHandleKeyboardEvents.value = false; + if (onBlur) { + onBlur(args); + } + }, + [onBlur, shouldHandleKeyboardEvents], + ); + + return ( + + ); +}; + +export const BottomSheetTextField = memo(BottomSheetTextFieldComponent); diff --git a/lib/ui/components/Callout.tsx b/lib/ui/components/Callout.tsx new file mode 100644 index 00000000..bfa246f0 --- /dev/null +++ b/lib/ui/components/Callout.tsx @@ -0,0 +1,60 @@ +import { StyleSheet, View } from 'react-native'; + +import { Col, ColProps } from '@lib/ui/components/Col'; +import { useStylesheet } from '@lib/ui/hooks/useStylesheet'; +import { Theme } from '@lib/ui/types/Theme'; + +import { lightTheme } from '../../../src/core/themes/light'; +import { ThemeContext } from '../contexts/ThemeContext'; + +export type CalloutProps = ColProps; + +export const Callout = ({ children, style, ...props }: CalloutProps) => { + const styles = useStylesheet(createStyles); + + return ( + + + {children} + + + + ); +}; + +const createStyles = ({ colors, shapes }: Theme) => + StyleSheet.create({ + container: { + maxWidth: 300, + maxHeight: 200, + backgroundColor: 'white', + borderRadius: shapes.md, + borderColor: colors.divider, + borderWidth: 1, + shadowColor: 'black', + shadowOpacity: 0.3, + shadowRadius: 8, + shadowOffset: { + width: 0, + height: 6, + }, + marginBottom: 6, + }, + arrow: { + position: 'absolute', + alignSelf: 'center', + bottom: -6, + width: 10, + height: 10, + backgroundColor: 'white', + borderColor: colors.divider, + borderRightWidth: 1, + borderBottomWidth: 1, + borderBottomRightRadius: 2, + transform: [ + { + rotateZ: '45deg', + }, + ], + }, + }); diff --git a/lib/ui/components/Card.tsx b/lib/ui/components/Card.tsx index 2498f412..eedc4aee 100644 --- a/lib/ui/components/Card.tsx +++ b/lib/ui/components/Card.tsx @@ -1,5 +1,6 @@ import { Platform, View, ViewProps } from 'react-native'; +import { IS_IOS } from '../../../src/core/constants'; import { useTheme } from '../hooks/useTheme'; export type CardProps = ViewProps & { @@ -22,6 +23,12 @@ export type CardProps = ViewProps & { * Toggles the inner spacing */ gapped?: boolean; + + /** + * If true, uses a semi-transparent background + * for use on translucent surfaces + */ + translucent?: boolean; }; /** @@ -31,6 +38,7 @@ export type CardProps = ViewProps & { export const Card = ({ children, style, + translucent = false, spaced = Platform.select({ ios: true, android: false }), rounded = Platform.select({ ios: true, android: false }), gapped = false, @@ -44,7 +52,8 @@ export const Card = ({ style={[ { borderRadius: rounded ? shapes.lg : undefined, - backgroundColor: colors.surface, + backgroundColor: + IS_IOS && translucent ? colors.translucentSurface : colors.surface, elevation: 2, marginHorizontal: spaced ? spacing[4] : undefined, marginVertical: spaced ? spacing[2] : undefined, diff --git a/lib/ui/components/Divider.tsx b/lib/ui/components/Divider.tsx index 0209c9ae..6b682b12 100644 --- a/lib/ui/components/Divider.tsx +++ b/lib/ui/components/Divider.tsx @@ -2,20 +2,26 @@ import { StyleSheet, View, ViewProps } from 'react-native'; import { useTheme } from '../hooks/useTheme'; -export type DividerProps = ViewProps; +export interface DividerProps extends ViewProps { + size?: number; +} /** * A divider element to separate list items */ -export const Divider = ({ style, ...props }: DividerProps) => { +export const Divider = ({ + size = StyleSheet.hairlineWidth, + style, + ...props +}: DividerProps) => { const { colors } = useTheme(); return ( { /** diff --git a/lib/ui/components/ListItem.tsx b/lib/ui/components/ListItem.tsx index c46739f2..43f515b6 100644 --- a/lib/ui/components/ListItem.tsx +++ b/lib/ui/components/ListItem.tsx @@ -16,7 +16,7 @@ import { NativeStackNavigationProp } from '@react-navigation/native-stack'; import { To } from '@react-navigation/native/lib/typescript/src/useLinkTo'; import { IS_IOS } from '../../../src/core/constants'; -import { GlobalStyles } from '../../../src/core/styles/globalStyles'; +import { GlobalStyles } from '../../../src/core/styles/GlobalStyles'; import { resolveLinkTo } from '../../../src/utils/resolveLinkTo'; import { useTheme } from '../hooks/useTheme'; import { DisclosureIndicator } from './DisclosureIndicator'; @@ -34,9 +34,10 @@ export interface ListItemProps extends TouchableHighlightProps { subtitleStyle?: StyleProp; isAction?: boolean; card?: boolean; + inverted?: boolean; titleProps?: TextProps; + multilineTitle?: boolean; unread?: boolean; - inverted?: boolean; } /** @@ -59,14 +60,65 @@ export const ListItem = ({ style, card, children, + inverted = false, + multilineTitle = false, titleProps, unread = false, - inverted = false, ...rest }: ListItemProps) => { const { fontSizes, fontWeights, colors, spacing } = useTheme(); const navigation = useNavigation>(); + const titleElement = + typeof title === 'string' ? ( + + {unread && } + + {title} + + + ) : ( + title + ); + + const subtitleElement = subtitle ? ( + typeof subtitle === 'string' ? ( + + {subtitle} + + ) : ( + subtitle + ) + ) : null; + return ( )} - {typeof title === 'string' ? ( - - {unread && } - - {title} - - - ) : ( - title - )} - {subtitle ? ( - typeof subtitle === 'string' ? ( - - {subtitle} - - ) : ( - subtitle - ) - ) : null} + {titleElement} + {subtitleElement} {!card && (!trailingItem && (linkTo || isAction) && IS_IOS ? ( diff --git a/lib/ui/components/OverviewList.tsx b/lib/ui/components/OverviewList.tsx index 7ea9c9f5..16d751c3 100644 --- a/lib/ui/components/OverviewList.tsx +++ b/lib/ui/components/OverviewList.tsx @@ -17,6 +17,7 @@ type Props = PropsWithChildren< loading?: boolean; indented?: boolean; emptyStateText?: string; + translucent?: boolean; } >; @@ -29,6 +30,7 @@ export const OverviewList = ({ loading = false, indented = false, dividers, + translucent = false, emptyStateText, style, ...rest @@ -40,6 +42,7 @@ export const OverviewList = ({ { + const styles = useStylesheet(createStyles); + + return ( + + + + + {children} + + + + ); +}; + +const createStyles = ({ fontWeights }: Theme) => + StyleSheet.create({ + text: { + fontWeight: fontWeights.medium, + }, + textNeutral: {}, + // eslint-disable-next-line react-native/no-color-literals + textPrimary: { + color: 'white', + }, + }); diff --git a/lib/ui/components/TextField.tsx b/lib/ui/components/TextField.tsx index 5a975a28..aa5b58e7 100644 --- a/lib/ui/components/TextField.tsx +++ b/lib/ui/components/TextField.tsx @@ -15,7 +15,7 @@ import { Theme } from '@lib/ui/types/Theme'; import { IS_IOS } from '../../../src/core/constants'; import { useTheme } from '../hooks/useTheme'; -export interface Props extends Omit { +export interface TextFieldProps extends Omit { inputRef?: Ref; label: string; type?: 'text' | 'password'; @@ -36,7 +36,7 @@ export const TextField = ({ numberOfLines = 1, autoCapitalize = 'none', ...rest -}: TextInputProps & Props) => { +}: TextFieldProps) => { const { colors } = useTheme(); const styles = useStylesheet(createStyles); diff --git a/lib/ui/components/TranslucentCard.tsx b/lib/ui/components/TranslucentCard.tsx new file mode 100644 index 00000000..84227a62 --- /dev/null +++ b/lib/ui/components/TranslucentCard.tsx @@ -0,0 +1,27 @@ +import { StyleSheet } from 'react-native'; + +import { Row, RowProps } from '@lib/ui/components/Row'; +import { useStylesheet } from '@lib/ui/hooks/useStylesheet'; +import { Theme } from '@lib/ui/types/Theme'; + +import { TranslucentView } from '../../../src/core/components/TranslucentView'; + +export const TranslucentCard = ({ children, ...props }: RowProps) => { + const styles = useStylesheet(createStyles); + + return ( + + + {children} + + ); +}; + +const createStyles = ({ shapes }: Theme) => + StyleSheet.create({ + container: { + borderRadius: shapes.lg, + overflow: 'hidden', + elevation: 12, + }, + }); diff --git a/lib/ui/components/TranslucentTextField.tsx b/lib/ui/components/TranslucentTextField.tsx index 52ec9af0..d2ed7f04 100644 --- a/lib/ui/components/TranslucentTextField.tsx +++ b/lib/ui/components/TranslucentTextField.tsx @@ -1,30 +1,54 @@ import { Platform, StyleSheet } from 'react-native'; -import { Props, TextField } from '@lib/ui/components/TextField'; +import { IconDefinition } from '@fortawesome/fontawesome-svg-core'; +import { faMagnifyingGlass } from '@fortawesome/free-solid-svg-icons'; +import { Icon } from '@lib/ui/components/Icon'; +import { Row } from '@lib/ui/components/Row'; +import { TextField, TextFieldProps } from '@lib/ui/components/TextField'; import { useStylesheet } from '@lib/ui/hooks/useStylesheet'; import { Theme } from '@lib/ui/types/Theme'; -export const TranslucentTextField = (props: Props) => { +export interface TranslucentTextFieldProps extends TextFieldProps { + leadingIcon?: IconDefinition; +} + +export const TranslucentTextField = ({ + style, + inputStyle, + leadingIcon = faMagnifyingGlass, + ...props +}: TranslucentTextFieldProps) => { const styles = useStylesheet(createStyles); return ( - + + {leadingIcon && } + + ); }; -const createStyles = ({ colors, shapes, spacing }: Theme) => +const createStyles = ({ colors, spacing }: Theme) => StyleSheet.create({ - textFieldInput: { + container: { backgroundColor: colors.translucentSurface, - borderRadius: shapes.xl, + borderRadius: 12, + }, + textField: { + flex: 1, paddingVertical: 0, - marginHorizontal: spacing[2], + }, + icon: { + opacity: 0.8, }, input: { margin: 0, + paddingLeft: 0, + paddingRight: spacing[2], borderBottomWidth: 0, paddingVertical: spacing[Platform.OS === 'ios' ? 2 : 1], }, diff --git a/package-lock.json b/package-lock.json index 9d4e2f68..5886bdd4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7,25 +7,29 @@ "": { "name": "@polito/students-app", "version": "1.4.0", + "hasInstallScript": true, "dependencies": { "@fortawesome/fontawesome-svg-core": "^6.2.1", "@fortawesome/free-brands-svg-icons": "^6.2.1", "@fortawesome/free-regular-svg-icons": "^6.2.1", "@fortawesome/free-solid-svg-icons": "^6.2.1", "@fortawesome/react-native-fontawesome": "^0.3.0", + "@gorhom/bottom-sheet": "^4.5.1", + "@kyupss/native-swipeable": "^1.0.1", "@miblanchard/react-native-slider": "^2.2.0", - "@polito/api-client": "^1.0.0-ALPHA.41", + "@polito/api-client": "^1.0.0-ALPHA.44", "@react-native-async-storage/async-storage": "^1.17.11", "@react-native-community/blur": "^4.3.0", "@react-native-community/geolocation": "^3.1.0", "@react-native-community/netinfo": "^9.3.4", - "@react-native-menu/menu": "^0.7.3", + "@react-native-menu/menu": "^0.8.0", "@react-navigation/bottom-tabs": "^6.5.8", "@react-navigation/elements": "^1.3.18", "@react-navigation/material-top-tabs": "^6.6.3", "@react-navigation/native": "^6.1.7", "@react-navigation/native-stack": "^6.9.13", "@react-navigation/stack": "^6.3.17", + "@rnmapbox/maps": "^10.0.8", "@sentry/react-native": "^5.9.1", "@tanstack/query-async-storage-persister": "^4.33.0", "@tanstack/react-query": "^4.29.18", @@ -53,7 +57,7 @@ "react-native-fast-image": "^8.6.3", "react-native-file-viewer": "^2.1.5", "react-native-fs": "^2.20.0", - "react-native-gesture-handler": "^2.7.1", + "react-native-gesture-handler": "^2.13.1", "react-native-html-to-pdf": "^0.12.0", "react-native-image-crop-picker": "^0.38.0", "react-native-keyboard-accessory": "^0.1.16", @@ -64,7 +68,9 @@ "react-native-pager-view": "^6.2.0", "react-native-path": "^0.0.5", "react-native-pdf": "^6.6.2", + "react-native-permissions": "^3.8.0", "react-native-progress": "^5.0.0", + "react-native-reanimated": "^2.17.0", "react-native-render-html": "^6.3.4", "react-native-safe-area-context": "^4.4.1", "react-native-screens": "^3.21.1", @@ -84,6 +90,7 @@ "@trivago/prettier-plugin-sort-imports": "^3.3.0", "@tsconfig/react-native": "^2.0.2", "@types/color": "^3.0.3", + "@types/geojson": "^7946.0.10", "@types/geopoint": "^1.0.1", "@types/jest": "^29.2.1", "@types/lodash": "^4.14.192", @@ -108,6 +115,7 @@ "jest": "^29.2.1", "lint-staged": "^13.0.3", "metro-react-native-babel-preset": "0.73.7", + "pod-install": "0.1.38", "prettier": "^2.7.1", "react-test-renderer": "18.0.0", "standard-version": "^9.5.0", @@ -380,7 +388,7 @@ } }, "node_modules/@babel/helper-plugin-utils": { - "version": "7.20.2", + "version": "7.22.5", "license": "MIT", "engines": { "node": ">=6.9.0" @@ -1330,6 +1338,19 @@ "@babel/core": "^7.0.0-0" } }, + "node_modules/@babel/plugin-transform-object-assign": { + "version": "7.22.5", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, "node_modules/@babel/plugin-transform-object-super": { "version": "7.18.6", "license": "MIT", @@ -1752,9 +1773,8 @@ } }, "node_modules/@babel/register/node_modules/semver": { - "version": "5.7.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", - "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", + "version": "5.7.1", + "license": "ISC", "bin": { "semver": "bin/semver" } @@ -1964,23 +1984,21 @@ } }, "node_modules/@commitlint/is-ignored": { - "version": "17.7.0", - "resolved": "https://registry.npmjs.org/@commitlint/is-ignored/-/is-ignored-17.7.0.tgz", - "integrity": "sha512-043rA7m45tyEfW7Zv2vZHF++176MLHH9h70fnPoYlB1slKBeKl8BwNIlnPg4xBdRBVNPaCqvXxWswx2GR4c9Hw==", + "version": "17.1.0", "dev": true, + "license": "MIT", "dependencies": { - "@commitlint/types": "^17.4.4", - "semver": "7.5.4" + "@commitlint/types": "^17.0.0", + "semver": "7.3.7" }, "engines": { "node": ">=v14" } }, "node_modules/@commitlint/is-ignored/node_modules/semver": { - "version": "7.5.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", - "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", + "version": "7.3.7", "dev": true, + "license": "ISC", "dependencies": { "lru-cache": "^6.0.0" }, @@ -2564,6 +2582,41 @@ "react-native-svg": ">= 11.x" } }, + "node_modules/@gorhom/bottom-sheet": { + "version": "4.5.1", + "license": "MIT", + "dependencies": { + "@gorhom/portal": "1.0.14", + "invariant": "^2.2.4" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-native": "*", + "react": "*", + "react-native": "*", + "react-native-gesture-handler": ">=1.10.1", + "react-native-reanimated": ">=2.2.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-native": { + "optional": true + } + } + }, + "node_modules/@gorhom/portal": { + "version": "1.0.14", + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.1" + }, + "peerDependencies": { + "react": "*", + "react-native": "*" + } + }, "node_modules/@hapi/hoek": { "version": "9.3.0", "license": "BSD-3-Clause" @@ -3948,6 +4001,17 @@ "react-native": "*" } }, + "node_modules/@kyupss/native-swipeable": { + "version": "1.0.1", + "license": "MIT", + "dependencies": { + "prop-types": "15.8.1" + }, + "peerDependencies": { + "deprecated-react-native-prop-types": ">=2.2.0", + "react-native": ">=0.69.0" + } + }, "node_modules/@miblanchard/react-native-slider": { "version": "2.3.1", "license": "MIT", @@ -4090,9 +4154,9 @@ } }, "node_modules/@polito/api-client": { - "version": "1.0.0-ALPHA.41", - "resolved": "https://npm.pkg.github.com/download/@polito/api-client/1.0.0-ALPHA.41/2fda5327df060c226dcada0e1f1e0af96626e8a9", - "integrity": "sha512-8dyJS4EvgxevRG48R98EA0tAuAN0iG8SXAs3R0T4TF3DjrJB0x08dIh6aYJs3QKydSYs2C6AwMa06M70jx7W5A==" + "version": "1.0.0-ALPHA.44", + "resolved": "https://npm.pkg.github.com/download/@polito/api-client/1.0.0-ALPHA.44/e8e7cdb5770298c649bfff4145cfa4a01f64b44c", + "integrity": "sha512-Cl0ohuEwCp5MlkYBdgf4VsYT4IU+ImF25BuL7JbVk5KIHAMJx8fNdoHVA4nvBvUM9j/qPx2qZbbiGYl3HonIAw==" }, "node_modules/@react-native-async-storage/async-storage": { "version": "1.17.11", @@ -4264,9 +4328,8 @@ } }, "node_modules/@react-native-community/cli-clean/node_modules/semver": { - "version": "5.7.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", - "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", + "version": "5.7.1", + "license": "ISC", "bin": { "semver": "bin/semver" } @@ -4519,9 +4582,8 @@ } }, "node_modules/@react-native-community/cli-doctor/node_modules/cross-spawn/node_modules/semver": { - "version": "5.7.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", - "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", + "version": "5.7.1", + "license": "ISC", "bin": { "semver": "bin/semver" } @@ -4823,9 +4885,8 @@ } }, "node_modules/@react-native-community/cli-platform-android/node_modules/semver": { - "version": "5.7.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", - "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", + "version": "5.7.1", + "license": "ISC", "bin": { "semver": "bin/semver" } @@ -4991,9 +5052,8 @@ } }, "node_modules/@react-native-community/cli-platform-ios/node_modules/semver": { - "version": "5.7.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", - "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", + "version": "5.7.1", + "license": "ISC", "bin": { "semver": "bin/semver" } @@ -5165,9 +5225,8 @@ } }, "node_modules/@react-native-community/cli-plugin-metro/node_modules/semver": { - "version": "5.7.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", - "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", + "version": "5.7.1", + "license": "ISC", "bin": { "semver": "bin/semver" } @@ -5360,9 +5419,8 @@ } }, "node_modules/@react-native-community/cli/node_modules/cross-spawn/node_modules/semver": { - "version": "5.7.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", - "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", + "version": "5.7.1", + "license": "ISC", "bin": { "semver": "bin/semver" } @@ -5558,7 +5616,7 @@ } }, "node_modules/@react-native-menu/menu": { - "version": "0.7.3", + "version": "0.8.0", "license": "MIT", "peerDependencies": { "react": "*", @@ -5579,8 +5637,7 @@ }, "node_modules/@react-navigation/bottom-tabs": { "version": "6.5.8", - "resolved": "https://registry.npmjs.org/@react-navigation/bottom-tabs/-/bottom-tabs-6.5.8.tgz", - "integrity": "sha512-0aa/jXea+LyBgR5NoRNWGKw0aFhjHwCkusigMRXIrCA4kINauDcAO0w0iFbZeKfaTCVAix5kK5UxDJJ2aJpevg==", + "license": "MIT", "dependencies": { "@react-navigation/elements": "^1.3.18", "color": "^4.2.3", @@ -5596,8 +5653,7 @@ }, "node_modules/@react-navigation/core": { "version": "6.4.9", - "resolved": "https://registry.npmjs.org/@react-navigation/core/-/core-6.4.9.tgz", - "integrity": "sha512-G9GH7bP9x0qqupxZnkSftnkn4JoXancElTvFc8FVGfEvxnxP+gBo3wqcknyBi7M5Vad4qecsYjCOa9wqsftv9g==", + "license": "MIT", "dependencies": { "@react-navigation/routers": "^6.1.9", "escape-string-regexp": "^4.0.0", @@ -5612,8 +5668,7 @@ }, "node_modules/@react-navigation/elements": { "version": "1.3.18", - "resolved": "https://registry.npmjs.org/@react-navigation/elements/-/elements-1.3.18.tgz", - "integrity": "sha512-/0hwnJkrr415yP0Hf4PjUKgGyfshrvNUKFXN85Mrt1gY49hy9IwxZgrrxlh0THXkPeq8q4VWw44eHDfAcQf20Q==", + "license": "MIT", "peerDependencies": { "@react-navigation/native": "^6.0.0", "react": "*", @@ -5623,8 +5678,7 @@ }, "node_modules/@react-navigation/material-top-tabs": { "version": "6.6.3", - "resolved": "https://registry.npmjs.org/@react-navigation/material-top-tabs/-/material-top-tabs-6.6.3.tgz", - "integrity": "sha512-7rbBUUvVSKD8jV/a7iV2BTSQ83G7W8grGSwBNojdeXdeZpsUa+wmmKnPtBFhdPv7DDQp7nzAYRx6RCOPtjZSCw==", + "license": "MIT", "dependencies": { "color": "^4.2.3", "warn-once": "^0.1.0" @@ -5639,8 +5693,7 @@ }, "node_modules/@react-navigation/native": { "version": "6.1.7", - "resolved": "https://registry.npmjs.org/@react-navigation/native/-/native-6.1.7.tgz", - "integrity": "sha512-W6E3+AtTombMucCRo6q7vPmluq8hSjS+IxfazJ/SokOe7ChJX7eLvvralIsJkjFj3iWV1KgOSnHxa6hdiFasBw==", + "license": "MIT", "dependencies": { "@react-navigation/core": "^6.4.9", "escape-string-regexp": "^4.0.0", @@ -5654,8 +5707,7 @@ }, "node_modules/@react-navigation/native-stack": { "version": "6.9.13", - "resolved": "https://registry.npmjs.org/@react-navigation/native-stack/-/native-stack-6.9.13.tgz", - "integrity": "sha512-ejlepMrvFneewL+XlXHHhn+6y3lwvavM4/R7XwBV0XJxCymujexK+7Vkg7UcvJ1lx4CRhOcyBSNfGmdNIHREyQ==", + "license": "MIT", "dependencies": { "@react-navigation/elements": "^1.3.18", "warn-once": "^0.1.0" @@ -5670,16 +5722,14 @@ }, "node_modules/@react-navigation/routers": { "version": "6.1.9", - "resolved": "https://registry.npmjs.org/@react-navigation/routers/-/routers-6.1.9.tgz", - "integrity": "sha512-lTM8gSFHSfkJvQkxacGM6VJtBt61ip2XO54aNfswD+KMw6eeZ4oehl7m0me3CR9hnDE4+60iAZR8sAhvCiI3NA==", + "license": "MIT", "dependencies": { "nanoid": "^3.1.23" } }, "node_modules/@react-navigation/stack": { "version": "6.3.17", - "resolved": "https://registry.npmjs.org/@react-navigation/stack/-/stack-6.3.17.tgz", - "integrity": "sha512-8/8ZvJROK3fp6PRmQ9MrXd9epBowA8NkfCaWW/N9H5arqwNX9lTXAkmcjicRhjpX+UNlMBR9dTLkWvPRe2vY9A==", + "license": "MIT", "dependencies": { "@react-navigation/elements": "^1.3.18", "color": "^4.2.3", @@ -5694,14 +5744,40 @@ "react-native-screens": ">= 3.0.0" } }, + "node_modules/@rnmapbox/maps": { + "version": "10.0.10", + "license": "MIT", + "dependencies": { + "@turf/along": "6.5.0", + "@turf/distance": "6.5.0", + "@turf/helpers": "6.5.0", + "@turf/length": "6.5.0", + "@turf/nearest-point-on-line": "6.5.0", + "@types/geojson": "^7946.0.7", + "debounce": "^1.2.0" + }, + "peerDependencies": { + "expo": ">=47.0.0", + "mapbox-gl": "^2.9.0", + "react": ">=16.6.1", + "react-native": ">=0.59.9" + }, + "peerDependenciesMeta": { + "expo": { + "optional": true + }, + "mapbox-gl": { + "optional": true + } + } + }, "node_modules/@sentry-internal/tracing": { - "version": "7.63.0", - "resolved": "https://registry.npmjs.org/@sentry-internal/tracing/-/tracing-7.63.0.tgz", - "integrity": "sha512-Fxpc53p6NGvLSURg3iRvZA0k10K9yfeVhtczvJnpX30POBuV41wxpkLHkb68fjksirjEma1K3Ut1iLOEEDpPQg==", + "version": "7.69.0", + "license": "MIT", "dependencies": { - "@sentry/core": "7.63.0", - "@sentry/types": "7.63.0", - "@sentry/utils": "7.63.0", + "@sentry/core": "7.69.0", + "@sentry/types": "7.69.0", + "@sentry/utils": "7.69.0", "tslib": "^2.4.1 || ^1.9.3" }, "engines": { @@ -5709,15 +5785,14 @@ } }, "node_modules/@sentry/browser": { - "version": "7.63.0", - "resolved": "https://registry.npmjs.org/@sentry/browser/-/browser-7.63.0.tgz", - "integrity": "sha512-P1Iw/2281C/7CUCRsN4jgXvjMNKnrwKqxRg7JqN8eVeCDPMpOeEPHNJ6YatEXdVLTKVn0JB7L63Q1prhFr8+SQ==", - "dependencies": { - "@sentry-internal/tracing": "7.63.0", - "@sentry/core": "7.63.0", - "@sentry/replay": "7.63.0", - "@sentry/types": "7.63.0", - "@sentry/utils": "7.63.0", + "version": "7.69.0", + "license": "MIT", + "dependencies": { + "@sentry-internal/tracing": "7.69.0", + "@sentry/core": "7.69.0", + "@sentry/replay": "7.69.0", + "@sentry/types": "7.69.0", + "@sentry/utils": "7.69.0", "tslib": "^2.4.1 || ^1.9.3" }, "engines": { @@ -5725,10 +5800,9 @@ } }, "node_modules/@sentry/cli": { - "version": "2.20.5", - "resolved": "https://registry.npmjs.org/@sentry/cli/-/cli-2.20.5.tgz", - "integrity": "sha512-ZvWb86eF0QXH9C5Mbi87aUmr8SH848yEpXJmlM2AoBowpE9kKDnewCAKvyXUihojUFwCSEEjoJhrRMMgmCZqXA==", + "version": "2.20.7", "hasInstallScript": true, + "license": "BSD-3-Clause", "dependencies": { "https-proxy-agent": "^5.0.0", "node-fetch": "^2.6.7", @@ -5744,12 +5818,11 @@ } }, "node_modules/@sentry/core": { - "version": "7.63.0", - "resolved": "https://registry.npmjs.org/@sentry/core/-/core-7.63.0.tgz", - "integrity": "sha512-13Ljiq8hv6ieCkO+Am99/PljYJO5ynKT/hRQrWgGy9IIEgUr8sV3fW+1W6K4/3MCeOJou0HsiGBjOD1mASItVg==", + "version": "7.69.0", + "license": "MIT", "dependencies": { - "@sentry/types": "7.63.0", - "@sentry/utils": "7.63.0", + "@sentry/types": "7.69.0", + "@sentry/utils": "7.69.0", "tslib": "^2.4.1 || ^1.9.3" }, "engines": { @@ -5757,13 +5830,12 @@ } }, "node_modules/@sentry/hub": { - "version": "7.63.0", - "resolved": "https://registry.npmjs.org/@sentry/hub/-/hub-7.63.0.tgz", - "integrity": "sha512-IDgNcoa+YiBbPrDwrZuzSP+vN9wvlQjLEL3OVn0OFaA6Q3X/Zs40JjRz4bTdKb9SjXbyeJ2boFr+4EFvGQoJ1Q==", + "version": "7.69.0", + "license": "MIT", "dependencies": { - "@sentry/core": "7.63.0", - "@sentry/types": "7.63.0", - "@sentry/utils": "7.63.0", + "@sentry/core": "7.69.0", + "@sentry/types": "7.69.0", + "@sentry/utils": "7.69.0", "tslib": "^2.4.1 || ^1.9.3" }, "engines": { @@ -5771,12 +5843,11 @@ } }, "node_modules/@sentry/integrations": { - "version": "7.63.0", - "resolved": "https://registry.npmjs.org/@sentry/integrations/-/integrations-7.63.0.tgz", - "integrity": "sha512-+P8GNqFZNH/yS/KPbvUfUDERneoRNUrqp9ayvvp8aq4cTtrBdM72CYgI21oG6cti42SSM1VDLYZomTV3ElPzSg==", + "version": "7.69.0", + "license": "MIT", "dependencies": { - "@sentry/types": "7.63.0", - "@sentry/utils": "7.63.0", + "@sentry/types": "7.69.0", + "@sentry/utils": "7.69.0", "localforage": "^1.8.1", "tslib": "^2.4.1 || ^1.9.3" }, @@ -5785,13 +5856,12 @@ } }, "node_modules/@sentry/react": { - "version": "7.63.0", - "resolved": "https://registry.npmjs.org/@sentry/react/-/react-7.63.0.tgz", - "integrity": "sha512-KFRjgADVE4aMI7gJmGnoSz65ZErQlz9xRB3vETWSyNOLprWXuQLPPtcDEn39BROtsDG4pLyYFaSDiD7o0+DyjQ==", + "version": "7.69.0", + "license": "MIT", "dependencies": { - "@sentry/browser": "7.63.0", - "@sentry/types": "7.63.0", - "@sentry/utils": "7.63.0", + "@sentry/browser": "7.69.0", + "@sentry/types": "7.69.0", + "@sentry/utils": "7.69.0", "hoist-non-react-statics": "^3.3.2", "tslib": "^2.4.1 || ^1.9.3" }, @@ -5803,18 +5873,17 @@ } }, "node_modules/@sentry/react-native": { - "version": "5.9.1", - "resolved": "https://registry.npmjs.org/@sentry/react-native/-/react-native-5.9.1.tgz", - "integrity": "sha512-L5Ah5cZ39b6iNlsHXHj7vxiTePGTxs+JWJPCyGYnmeleh0IQaDedDJJgYwB78D8mGuq4JnTOY5B9BrFKBtLLlg==", - "dependencies": { - "@sentry/browser": "7.63.0", - "@sentry/cli": "2.20.5", - "@sentry/core": "7.63.0", - "@sentry/hub": "7.63.0", - "@sentry/integrations": "7.63.0", - "@sentry/react": "7.63.0", - "@sentry/types": "7.63.0", - "@sentry/utils": "7.63.0" + "version": "5.10.0", + "license": "MIT", + "dependencies": { + "@sentry/browser": "7.69.0", + "@sentry/cli": "2.20.7", + "@sentry/core": "7.69.0", + "@sentry/hub": "7.69.0", + "@sentry/integrations": "7.69.0", + "@sentry/react": "7.69.0", + "@sentry/types": "7.69.0", + "@sentry/utils": "7.69.0" }, "peerDependencies": { "react": ">=17.0.0", @@ -5822,32 +5891,29 @@ } }, "node_modules/@sentry/replay": { - "version": "7.63.0", - "resolved": "https://registry.npmjs.org/@sentry/replay/-/replay-7.63.0.tgz", - "integrity": "sha512-ikeFVojuP9oDF103blZcj0Vvb4S50dV54BESMrMW2lYBoMMjvOd7AdL+iDHjn1OL05/mv1C6Oc8MovmvdjILVA==", + "version": "7.69.0", + "license": "MIT", "dependencies": { - "@sentry/core": "7.63.0", - "@sentry/types": "7.63.0", - "@sentry/utils": "7.63.0" + "@sentry/core": "7.69.0", + "@sentry/types": "7.69.0", + "@sentry/utils": "7.69.0" }, "engines": { "node": ">=12" } }, "node_modules/@sentry/types": { - "version": "7.63.0", - "resolved": "https://registry.npmjs.org/@sentry/types/-/types-7.63.0.tgz", - "integrity": "sha512-pZNwJVW7RqNLGuTUAhoygt0c9zmc0js10eANAz0MstygJRhQI1tqPDuiELVdujPrbeL+IFKF+7NvRDAydR2Niw==", + "version": "7.69.0", + "license": "MIT", "engines": { "node": ">=8" } }, "node_modules/@sentry/utils": { - "version": "7.63.0", - "resolved": "https://registry.npmjs.org/@sentry/utils/-/utils-7.63.0.tgz", - "integrity": "sha512-7FQv1RYAwnuTuarruP+1+Jd6YQuN7i/Y7KltwPMVEwU7j5mzYQaexLr/Jz1XIdR2KYVdkbXQyP8jj8BmA6u9Jw==", + "version": "7.69.0", + "license": "MIT", "dependencies": { - "@sentry/types": "7.63.0", + "@sentry/types": "7.69.0", "tslib": "^2.4.1 || ^1.9.3" }, "engines": { @@ -5888,11 +5954,10 @@ } }, "node_modules/@tanstack/query-async-storage-persister": { - "version": "4.33.0", - "resolved": "https://registry.npmjs.org/@tanstack/query-async-storage-persister/-/query-async-storage-persister-4.33.0.tgz", - "integrity": "sha512-kauRnVFETDuZ7TWLPPwcs6Ilbxcs2qK/M/r9QX3Hynf2SVwnQ1Qy0XPC79mdAcxgUrjTivVbNW8YV6rzq3t+dA==", + "version": "4.35.7", + "license": "MIT", "dependencies": { - "@tanstack/query-persist-client-core": "4.33.0" + "@tanstack/query-persist-client-core": "4.35.7" }, "funding": { "type": "github", @@ -5900,20 +5965,18 @@ } }, "node_modules/@tanstack/query-core": { - "version": "4.33.0", - "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-4.33.0.tgz", - "integrity": "sha512-qYu73ptvnzRh6se2nyBIDHGBQvPY1XXl3yR769B7B6mIDD7s+EZhdlWHQ67JI6UOTFRaI7wupnTnwJ3gE0Mr/g==", + "version": "4.35.7", + "license": "MIT", "funding": { "type": "github", "url": "https://github.com/sponsors/tannerlinsley" } }, "node_modules/@tanstack/query-persist-client-core": { - "version": "4.33.0", - "resolved": "https://registry.npmjs.org/@tanstack/query-persist-client-core/-/query-persist-client-core-4.33.0.tgz", - "integrity": "sha512-3P16+2JjcUU5CHi10jJuwd0ZQYvQtSuzLvCUCjVuAnj3GZjfSso1v8t6WAObAr9RPuIC6vDXeOQ3mr07EF/NxQ==", + "version": "4.35.7", + "license": "MIT", "dependencies": { - "@tanstack/query-core": "4.33.0" + "@tanstack/query-core": "4.35.7" }, "funding": { "type": "github", @@ -5921,11 +5984,10 @@ } }, "node_modules/@tanstack/react-query": { - "version": "4.33.0", - "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-4.33.0.tgz", - "integrity": "sha512-97nGbmDK0/m0B86BdiXzx3EW9RcDYKpnyL2+WwyuLHEgpfThYAnXFaMMmnTDuAO4bQJXEhflumIEUfKmP7ESGA==", + "version": "4.35.7", + "license": "MIT", "dependencies": { - "@tanstack/query-core": "4.33.0", + "@tanstack/query-core": "4.35.7", "use-sync-external-store": "^1.2.0" }, "funding": { @@ -5947,18 +6009,17 @@ } }, "node_modules/@tanstack/react-query-persist-client": { - "version": "4.33.0", - "resolved": "https://registry.npmjs.org/@tanstack/react-query-persist-client/-/react-query-persist-client-4.33.0.tgz", - "integrity": "sha512-B3q0r1tqTTSkd9vctyqFj28xdGZJ+Dnr/7H05Ta1JF1w7EauVQl8ILrmXADecwvILnr1xoZO6lvi2W+mZxMinw==", + "version": "4.35.7", + "license": "MIT", "dependencies": { - "@tanstack/query-persist-client-core": "4.33.0" + "@tanstack/query-persist-client-core": "4.35.7" }, "funding": { "type": "github", "url": "https://github.com/sponsors/tannerlinsley" }, "peerDependencies": { - "@tanstack/react-query": "^4.33.0" + "@tanstack/react-query": "^4.35.7" } }, "node_modules/@trivago/prettier-plugin-sort-imports": { @@ -6096,6 +6157,145 @@ "dev": true, "license": "MIT" }, + "node_modules/@turf/along": { + "version": "6.5.0", + "license": "MIT", + "dependencies": { + "@turf/bearing": "^6.5.0", + "@turf/destination": "^6.5.0", + "@turf/distance": "^6.5.0", + "@turf/helpers": "^6.5.0", + "@turf/invariant": "^6.5.0" + }, + "funding": { + "url": "https://opencollective.com/turf" + } + }, + "node_modules/@turf/bbox": { + "version": "6.5.0", + "license": "MIT", + "dependencies": { + "@turf/helpers": "^6.5.0", + "@turf/meta": "^6.5.0" + }, + "funding": { + "url": "https://opencollective.com/turf" + } + }, + "node_modules/@turf/bearing": { + "version": "6.5.0", + "license": "MIT", + "dependencies": { + "@turf/helpers": "^6.5.0", + "@turf/invariant": "^6.5.0" + }, + "funding": { + "url": "https://opencollective.com/turf" + } + }, + "node_modules/@turf/destination": { + "version": "6.5.0", + "license": "MIT", + "dependencies": { + "@turf/helpers": "^6.5.0", + "@turf/invariant": "^6.5.0" + }, + "funding": { + "url": "https://opencollective.com/turf" + } + }, + "node_modules/@turf/distance": { + "version": "6.5.0", + "license": "MIT", + "dependencies": { + "@turf/helpers": "^6.5.0", + "@turf/invariant": "^6.5.0" + }, + "funding": { + "url": "https://opencollective.com/turf" + } + }, + "node_modules/@turf/helpers": { + "version": "6.5.0", + "license": "MIT", + "funding": { + "url": "https://opencollective.com/turf" + } + }, + "node_modules/@turf/invariant": { + "version": "6.5.0", + "license": "MIT", + "dependencies": { + "@turf/helpers": "^6.5.0" + }, + "funding": { + "url": "https://opencollective.com/turf" + } + }, + "node_modules/@turf/length": { + "version": "6.5.0", + "license": "MIT", + "dependencies": { + "@turf/distance": "^6.5.0", + "@turf/helpers": "^6.5.0", + "@turf/meta": "^6.5.0" + }, + "funding": { + "url": "https://opencollective.com/turf" + } + }, + "node_modules/@turf/line-intersect": { + "version": "6.5.0", + "license": "MIT", + "dependencies": { + "@turf/helpers": "^6.5.0", + "@turf/invariant": "^6.5.0", + "@turf/line-segment": "^6.5.0", + "@turf/meta": "^6.5.0", + "geojson-rbush": "3.x" + }, + "funding": { + "url": "https://opencollective.com/turf" + } + }, + "node_modules/@turf/line-segment": { + "version": "6.5.0", + "license": "MIT", + "dependencies": { + "@turf/helpers": "^6.5.0", + "@turf/invariant": "^6.5.0", + "@turf/meta": "^6.5.0" + }, + "funding": { + "url": "https://opencollective.com/turf" + } + }, + "node_modules/@turf/meta": { + "version": "6.5.0", + "license": "MIT", + "dependencies": { + "@turf/helpers": "^6.5.0" + }, + "funding": { + "url": "https://opencollective.com/turf" + } + }, + "node_modules/@turf/nearest-point-on-line": { + "version": "6.5.0", + "license": "MIT", + "dependencies": { + "@turf/bearing": "^6.5.0", + "@turf/destination": "^6.5.0", + "@turf/distance": "^6.5.0", + "@turf/helpers": "^6.5.0", + "@turf/invariant": "^6.5.0", + "@turf/line-intersect": "^6.5.0", + "@turf/meta": "^6.5.0" + }, + "funding": { + "url": "https://opencollective.com/turf" + } + }, "node_modules/@types/babel__core": { "version": "7.20.0", "dev": true, @@ -6154,6 +6354,10 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/geojson": { + "version": "7946.0.10", + "license": "MIT" + }, "node_modules/@types/geopoint": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/@types/geopoint/-/geopoint-1.0.1.tgz", @@ -6427,10 +6631,9 @@ } }, "node_modules/@typescript-eslint/eslint-plugin/node_modules/semver": { - "version": "7.5.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", - "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", + "version": "7.3.8", "dev": true, + "license": "ISC", "dependencies": { "lru-cache": "^6.0.0" }, @@ -6564,10 +6767,9 @@ } }, "node_modules/@typescript-eslint/type-utils/node_modules/semver": { - "version": "7.5.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", - "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", + "version": "7.3.8", "dev": true, + "license": "ISC", "dependencies": { "lru-cache": "^6.0.0" }, @@ -6617,10 +6819,9 @@ } }, "node_modules/@typescript-eslint/typescript-estree/node_modules/semver": { - "version": "7.5.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", - "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", + "version": "7.3.8", "dev": true, + "license": "ISC", "dependencies": { "lru-cache": "^6.0.0" }, @@ -6727,10 +6928,9 @@ } }, "node_modules/@typescript-eslint/utils/node_modules/semver": { - "version": "7.5.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", - "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", + "version": "7.3.8", "dev": true, + "license": "ISC", "dependencies": { "lru-cache": "^6.0.0" }, @@ -6807,8 +7007,7 @@ }, "node_modules/agent-base": { "version": "6.0.2", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", - "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "license": "MIT", "dependencies": { "debug": "4" }, @@ -8264,10 +8463,9 @@ } }, "node_modules/conventional-changelog-core/node_modules/semver": { - "version": "5.7.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", - "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", + "version": "5.7.1", "dev": true, + "license": "ISC", "bin": { "semver": "bin/semver" } @@ -8429,8 +8627,7 @@ }, "node_modules/copy-anything": { "version": "3.0.5", - "resolved": "https://registry.npmjs.org/copy-anything/-/copy-anything-3.0.5.tgz", - "integrity": "sha512-yCEafptTtb4bk7GLEQoM8KVJpxAfdBJYaXyzQEgQQQgYrZiDp8SJmGKlYza6CYjEDNstAdNdKA3UuoULlEbS6w==", + "license": "MIT", "dependencies": { "is-what": "^4.1.8" }, @@ -8608,6 +8805,10 @@ "version": "1.11.7", "license": "MIT" }, + "node_modules/debounce": { + "version": "1.2.1", + "license": "MIT" + }, "node_modules/debug": { "version": "4.3.4", "license": "MIT", @@ -9193,10 +9394,9 @@ } }, "node_modules/eslint-plugin-jsdoc/node_modules/semver": { - "version": "7.5.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", - "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", + "version": "7.3.8", "dev": true, + "license": "ISC", "dependencies": { "lru-cache": "^6.0.0" }, @@ -9918,8 +10118,7 @@ }, "node_modules/filter-obj": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/filter-obj/-/filter-obj-1.1.0.tgz", - "integrity": "sha512-8rXg1ZnX7xzy2NGDVkBVaAy+lSlPNwad13BtgSlLuxfIslyt5Vg64U7tFcCt4WS1R0hvtnQybT/IyCkGZ3DpXQ==", + "license": "MIT", "engines": { "node": ">=0.10.0" } @@ -10056,9 +10255,8 @@ } }, "node_modules/find-cache-dir/node_modules/semver": { - "version": "5.7.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", - "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", + "version": "5.7.1", + "license": "ISC", "bin": { "semver": "bin/semver" } @@ -10196,6 +10394,21 @@ "node": ">=6.9.0" } }, + "node_modules/geojson-rbush": { + "version": "3.2.0", + "license": "MIT", + "dependencies": { + "@turf/bbox": "*", + "@turf/helpers": "6.x", + "@turf/meta": "6.x", + "@types/geojson": "7946.0.8", + "rbush": "^3.0.1" + } + }, + "node_modules/geojson-rbush/node_modules/@types/geojson": { + "version": "7946.0.8", + "license": "MIT" + }, "node_modules/geopoint": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/geopoint/-/geopoint-1.0.1.tgz", @@ -10734,8 +10947,7 @@ }, "node_modules/https-proxy-agent": { "version": "5.0.1", - "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", - "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "license": "MIT", "dependencies": { "agent-base": "6", "debug": "4" @@ -10840,8 +11052,7 @@ }, "node_modules/immediate": { "version": "3.0.6", - "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz", - "integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==" + "license": "MIT" }, "node_modules/import-fresh": { "version": "3.3.0", @@ -11429,8 +11640,7 @@ }, "node_modules/is-what": { "version": "4.1.15", - "resolved": "https://registry.npmjs.org/is-what/-/is-what-4.1.15.tgz", - "integrity": "sha512-uKua1wfy3Yt+YqsD6mTUEa2zSi3G1oPlqTflgaPJ7z63vUGN5pxFpnQfeSLMFnJDEsdvOtkp1rUWkYjB4YfhgA==", + "license": "MIT", "engines": { "node": ">=12.13" }, @@ -13429,10 +13639,9 @@ } }, "node_modules/jest-runtime/node_modules/semver": { - "version": "7.5.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", - "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", + "version": "7.3.8", "dev": true, + "license": "ISC", "dependencies": { "lru-cache": "^6.0.0" }, @@ -13603,10 +13812,9 @@ "license": "MIT" }, "node_modules/jest-snapshot/node_modules/semver": { - "version": "7.5.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", - "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", + "version": "7.3.8", "dev": true, + "license": "ISC", "dependencies": { "lru-cache": "^6.0.0" }, @@ -14496,8 +14704,7 @@ }, "node_modules/lie": { "version": "3.1.1", - "resolved": "https://registry.npmjs.org/lie/-/lie-3.1.1.tgz", - "integrity": "sha512-RiNhHysUjhrDQntfYSfY4MU24coXXdEOgw9WGcKHNeEwffDYbF//u87M1EWaMGzuFoSbqW0C9C6lEEhDOAswfw==", + "license": "MIT", "dependencies": { "immediate": "~3.0.5" } @@ -14646,6 +14853,14 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/lint-staged/node_modules/yaml": { + "version": "2.1.3", + "dev": true, + "license": "ISC", + "engines": { + "node": ">= 14" + } + }, "node_modules/listr2": { "version": "4.0.5", "dev": true, @@ -14808,8 +15023,7 @@ }, "node_modules/localforage": { "version": "1.10.0", - "resolved": "https://registry.npmjs.org/localforage/-/localforage-1.10.0.tgz", - "integrity": "sha512-14/H1aX7hzBBmmh7sGPd+AOMkkIrHM3Z1PAyGgZigA1H1p5O5ANnMyWzvpAETtG68/dC4pC0ncy3+PPGzXZHPg==", + "license": "Apache-2.0", "dependencies": { "lie": "3.1.1" } @@ -14840,6 +15054,10 @@ "version": "4.0.8", "license": "MIT" }, + "node_modules/lodash.isequal": { + "version": "4.5.0", + "license": "MIT" + }, "node_modules/lodash.ismatch": { "version": "4.4.0", "dev": true, @@ -16307,10 +16525,9 @@ } }, "node_modules/normalize-package-data/node_modules/semver": { - "version": "7.5.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", - "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", + "version": "7.3.8", "dev": true, + "license": "ISC", "dependencies": { "lru-cache": "^6.0.0" }, @@ -17004,6 +17221,14 @@ "node": ">=6" } }, + "node_modules/pod-install": { + "version": "0.1.38", + "dev": true, + "license": "MIT", + "bin": { + "pod-install": "build/index.js" + } + }, "node_modules/point-in-polygon": { "version": "1.1.0", "license": "MIT" @@ -17091,8 +17316,7 @@ }, "node_modules/progress": { "version": "2.0.3", - "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", - "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", + "license": "MIT", "engines": { "node": ">=0.4.0" } @@ -17126,8 +17350,7 @@ }, "node_modules/proxy-from-env": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", - "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==" + "license": "MIT" }, "node_modules/pump": { "version": "3.0.0", @@ -17156,8 +17379,7 @@ }, "node_modules/query-string": { "version": "7.1.3", - "resolved": "https://registry.npmjs.org/query-string/-/query-string-7.1.3.tgz", - "integrity": "sha512-hh2WYhq4fi8+b+/2Kg9CEge4fDPvHS534aOOvOZeQ3+Vf2mCFsaFBYj0i+iXcAq6I9Vzp5fjMFBlONvayDC1qg==", + "license": "MIT", "dependencies": { "decode-uri-component": "^0.2.2", "filter-obj": "^1.1.0", @@ -17198,6 +17420,10 @@ "node": ">=8" } }, + "node_modules/quickselect": { + "version": "2.0.0", + "license": "ISC" + }, "node_modules/ramda": { "version": "0.27.2", "license": "MIT" @@ -17209,6 +17435,13 @@ "node": ">= 0.6" } }, + "node_modules/rbush": { + "version": "3.0.1", + "license": "MIT", + "dependencies": { + "quickselect": "^2.0.0" + } + }, "node_modules/react": { "version": "18.2.0", "license": "MIT", @@ -17431,7 +17664,7 @@ } }, "node_modules/react-native-gesture-handler": { - "version": "2.9.0", + "version": "2.13.1", "license": "MIT", "dependencies": { "@egjs/hammerjs": "^2.0.17", @@ -17510,8 +17743,7 @@ }, "node_modules/react-native-override-color-scheme": { "version": "1.0.3", - "resolved": "https://registry.npmjs.org/react-native-override-color-scheme/-/react-native-override-color-scheme-1.0.3.tgz", - "integrity": "sha512-AMHR0uOMbEJL59m61kl0CMcLKRC+dgo6uozNLd2S70o0kUDMIDhr+EF484/af8UDB22OHI5ehsprGMKLiXIfow==", + "license": "MIT", "peerDependencies": { "react": "*", "react-native": "*" @@ -17519,8 +17751,7 @@ }, "node_modules/react-native-pager-view": { "version": "6.2.0", - "resolved": "https://registry.npmjs.org/react-native-pager-view/-/react-native-pager-view-6.2.0.tgz", - "integrity": "sha512-pf9OnL/Tkr+5s4Gjmsn7xh91PtJLDa6qxYa/bmtUhd/+s4cQdWQ8DIFoOFghwZIHHHwVdWtoXkp6HtpjN+r20g==", + "license": "MIT", "peerDependencies": { "react": "*", "react-native": "*" @@ -17538,6 +17769,34 @@ "deprecated-react-native-prop-types": "^2.3.0" } }, + "node_modules/react-native-permissions": { + "version": "3.8.4", + "license": "MIT", + "dependencies": { + "picocolors": "^1.0.0", + "pkg-dir": "^5.0.0" + }, + "peerDependencies": { + "react": ">=16.13.1", + "react-native": ">=0.63.3", + "react-native-windows": ">=0.62.0" + }, + "peerDependenciesMeta": { + "react-native-windows": { + "optional": true + } + } + }, + "node_modules/react-native-permissions/node_modules/pkg-dir": { + "version": "5.0.0", + "license": "MIT", + "dependencies": { + "find-up": "^5.0.0" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/react-native-progress": { "version": "5.0.0", "license": "MIT", @@ -17548,6 +17807,23 @@ "react-native-svg": "*" } }, + "node_modules/react-native-reanimated": { + "version": "2.17.0", + "license": "MIT", + "dependencies": { + "@babel/plugin-transform-object-assign": "^7.16.7", + "@babel/preset-typescript": "^7.16.7", + "invariant": "^2.2.4", + "lodash.isequal": "^4.5.0", + "setimmediate": "^1.0.5", + "string-hash-64": "^1.0.3" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0", + "react": "*", + "react-native": "*" + } + }, "node_modules/react-native-render-html": { "version": "6.3.4", "license": "BSD-2-Clause", @@ -17577,8 +17853,7 @@ }, "node_modules/react-native-screens": { "version": "3.22.0", - "resolved": "https://registry.npmjs.org/react-native-screens/-/react-native-screens-3.22.0.tgz", - "integrity": "sha512-csLypBSXIt/egh37YJmokETptZJCtZdoZBsZNLR9n31GesDyVogprT+MM22dEPDuxPxt/mFWq+lSpVwk7khuTw==", + "license": "MIT", "dependencies": { "react-freeze": "^1.0.0", "warn-once": "^0.1.0" @@ -17602,8 +17877,7 @@ }, "node_modules/react-native-tab-view": { "version": "3.5.2", - "resolved": "https://registry.npmjs.org/react-native-tab-view/-/react-native-tab-view-3.5.2.tgz", - "integrity": "sha512-nE5WqjbeEPsWQx4mtz81QGVvgHRhujTNIIZiMCx3Bj6CBFDafbk7XZp9ocmtzXUQaZ4bhtVS43R4FIiR4LboJw==", + "license": "MIT", "dependencies": { "use-latest-callback": "^0.1.5" }, @@ -17659,8 +17933,7 @@ }, "node_modules/react-string-replace": { "version": "1.1.1", - "resolved": "https://registry.npmjs.org/react-string-replace/-/react-string-replace-1.1.1.tgz", - "integrity": "sha512-26TUbLzLfHQ5jO5N7y3Mx88eeKo0Ml0UjCQuX4BMfOd/JX+enQqlKpL1CZnmjeBRvQE8TR+ds9j1rqx9CxhKHQ==", + "license": "MIT", "engines": { "node": ">=0.12.0" } @@ -17812,10 +18085,9 @@ } }, "node_modules/read-pkg/node_modules/semver": { - "version": "5.7.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", - "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", + "version": "5.7.1", "dev": true, + "license": "ISC", "bin": { "semver": "bin/semver" } @@ -18183,9 +18455,8 @@ } }, "node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "version": "6.3.0", + "license": "ISC", "bin": { "semver": "bin/semver.js" } @@ -18308,6 +18579,10 @@ "node": ">=0.10.0" } }, + "node_modules/setimmediate": { + "version": "1.0.5", + "license": "MIT" + }, "node_modules/setprototypeof": { "version": "1.2.0", "license": "ISC" @@ -18654,8 +18929,7 @@ }, "node_modules/split-on-first": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/split-on-first/-/split-on-first-1.1.0.tgz", - "integrity": "sha512-43ZssAJaMusuKWL8sKUBQXHWOpq8d6CfN/u1p4gUzfJkM05C8rxTmYrkIPTXapZpORA6LkkzcUulJ8FqA7Uudw==", + "license": "MIT", "engines": { "node": ">=6" } @@ -18784,10 +19058,9 @@ } }, "node_modules/standard-version/node_modules/semver": { - "version": "7.5.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", - "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", + "version": "7.3.8", "dev": true, + "license": "ISC", "dependencies": { "lru-cache": "^6.0.0" }, @@ -18917,8 +19190,7 @@ }, "node_modules/strict-uri-encode": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/strict-uri-encode/-/strict-uri-encode-2.0.0.tgz", - "integrity": "sha512-QwiXZgpRcKkhTj2Scnn++4PKtWsH0kpzZ62L2R6c/LUVYv7hVnZqcg2+sMuT6R7Jusu1vviK/MFsu6kNJfWlEQ==", + "license": "MIT", "engines": { "node": ">=4" } @@ -18956,6 +19228,10 @@ "node": ">=0.6.19" } }, + "node_modules/string-hash-64": { + "version": "1.0.3", + "license": "MIT" + }, "node_modules/string-length": { "version": "4.0.2", "dev": true, @@ -19131,9 +19407,8 @@ "license": "MIT" }, "node_modules/superjson": { - "version": "1.13.1", - "resolved": "https://registry.npmjs.org/superjson/-/superjson-1.13.1.tgz", - "integrity": "sha512-AVH2eknm9DEd3qvxM4Sq+LTCkSXE2ssfh1t11MHMXyYXFQyQ1HLgVvV+guLTsaQnJU3gnaVo34TohHPulY/wLg==", + "version": "1.13.3", + "license": "MIT", "dependencies": { "copy-anything": "^3.0.2" }, @@ -19378,8 +19653,7 @@ }, "node_modules/tslib": { "version": "2.6.2", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", - "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" + "license": "0BSD" }, "node_modules/tsutils": { "version": "3.21.0", @@ -19770,10 +20044,9 @@ "license": "ISC" }, "node_modules/word-wrap": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", - "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "version": "1.2.3", "dev": true, + "license": "MIT", "engines": { "node": ">=0.10.0" } @@ -19902,15 +20175,6 @@ "dev": true, "license": "ISC" }, - "node_modules/yaml": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.3.1.tgz", - "integrity": "sha512-2eHWfjaoXgTBC2jNM1LRef62VQa0umtvRiDSk6HSzW7RvS5YtkabJrwYLLEKWBc8a5U2PTSCs+dJjUTJdlHsWQ==", - "dev": true, - "engines": { - "node": ">= 14" - } - }, "node_modules/yargs": { "version": "17.6.0", "license": "MIT", diff --git a/package.json b/package.json index af4b7346..ccfef172 100644 --- a/package.json +++ b/package.json @@ -15,7 +15,8 @@ "format:check": "prettier --check '**/*.{ts,tsx,md}'", "format": "prettier --write '**/*.{ts,tsx,md}'", "types:check": "tsc --noEmit", - "check": "npm run lint:check && npm run format:check && npm run types:check" + "check": "npm run lint:check && npm run format:check && npm run types:check", + "postinstall": "react-native setup-ios-permissions && pod-install" }, "dependencies": { "@fortawesome/fontawesome-svg-core": "^6.2.1", @@ -23,19 +24,22 @@ "@fortawesome/free-regular-svg-icons": "^6.2.1", "@fortawesome/free-solid-svg-icons": "^6.2.1", "@fortawesome/react-native-fontawesome": "^0.3.0", + "@gorhom/bottom-sheet": "^4.5.1", + "@kyupss/native-swipeable": "^1.0.1", "@miblanchard/react-native-slider": "^2.2.0", - "@polito/api-client": "^1.0.0-ALPHA.41", + "@polito/api-client": "^1.0.0-ALPHA.44", "@react-native-async-storage/async-storage": "^1.17.11", "@react-native-community/blur": "^4.3.0", "@react-native-community/geolocation": "^3.1.0", "@react-native-community/netinfo": "^9.3.4", - "@react-native-menu/menu": "^0.7.3", + "@react-native-menu/menu": "^0.8.0", "@react-navigation/bottom-tabs": "^6.5.8", "@react-navigation/elements": "^1.3.18", "@react-navigation/material-top-tabs": "^6.6.3", "@react-navigation/native": "^6.1.7", "@react-navigation/native-stack": "^6.9.13", "@react-navigation/stack": "^6.3.17", + "@rnmapbox/maps": "^10.0.8", "@sentry/react-native": "^5.9.1", "@tanstack/query-async-storage-persister": "^4.33.0", "@tanstack/react-query": "^4.29.18", @@ -63,7 +67,7 @@ "react-native-fast-image": "^8.6.3", "react-native-file-viewer": "^2.1.5", "react-native-fs": "^2.20.0", - "react-native-gesture-handler": "^2.7.1", + "react-native-gesture-handler": "^2.13.1", "react-native-html-to-pdf": "^0.12.0", "react-native-image-crop-picker": "^0.38.0", "react-native-keyboard-accessory": "^0.1.16", @@ -74,7 +78,9 @@ "react-native-pager-view": "^6.2.0", "react-native-path": "^0.0.5", "react-native-pdf": "^6.6.2", + "react-native-permissions": "^3.8.0", "react-native-progress": "^5.0.0", + "react-native-reanimated": "^2.17.0", "react-native-render-html": "^6.3.4", "react-native-safe-area-context": "^4.4.1", "react-native-screens": "^3.21.1", @@ -94,6 +100,7 @@ "@trivago/prettier-plugin-sort-imports": "^3.3.0", "@tsconfig/react-native": "^2.0.2", "@types/color": "^3.0.3", + "@types/geojson": "^7946.0.10", "@types/geopoint": "^1.0.1", "@types/jest": "^29.2.1", "@types/lodash": "^4.14.192", @@ -118,11 +125,16 @@ "jest": "^29.2.1", "lint-staged": "^13.0.3", "metro-react-native-babel-preset": "0.73.7", + "pod-install": "0.1.38", "prettier": "^2.7.1", "react-test-renderer": "18.0.0", "standard-version": "^9.5.0", "typescript": "^4.8.4" }, + "reactNativePermissionsIOS": [ + "LocationAccuracy", + "LocationWhenInUse" + ], "jest": { "preset": "react-native", "moduleFileExtensions": [ diff --git a/src/App.tsx b/src/App.tsx index 44e02969..f23c0fa7 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,5 +1,9 @@ +import { GestureHandlerRootView } from 'react-native-gesture-handler'; + import { SafeAreaProvider } from 'react-native-safe-area-context'; +import { BottomSheetModalProvider } from '@gorhom/bottom-sheet'; +import Mapbox from '@rnmapbox/maps'; import * as Sentry from '@sentry/react-native'; import { AppContent } from './core/components/AppContent'; @@ -9,30 +13,37 @@ import { FeedbackProvider } from './core/providers/FeedbackProvider'; import { PreferencesProvider } from './core/providers/PreferencesProvider'; import { SplashProvider } from './core/providers/SplashProvider'; import { UiProvider } from './core/providers/UiProvider'; +import { GlobalStyles } from './core/styles/GlobalStyles'; import { initSentry } from './utils/sentry'; import { extendSuperJSON } from './utils/superjson'; initSentry(); extendSuperJSON(); +Mapbox.setAccessToken(process.env.MAPBOX_TOKEN!); + export const App = () => { return ( - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + ); }; diff --git a/src/core/components/HeaderLogo.tsx b/src/core/components/HeaderLogo.tsx index 2b53a787..c776bbcb 100644 --- a/src/core/components/HeaderLogo.tsx +++ b/src/core/components/HeaderLogo.tsx @@ -9,7 +9,7 @@ export const HeaderLogo = (props: RowProps) => { const { palettes, dark } = useTheme(); return ( - + void; favoriteServices: string[]; peopleSearched: PersonOverview[]; -}; + placesSearched: PlaceOverview[]; +} + +export interface PreferencesContextProps extends PreferencesContextBase { + updatePreference: ( + key: T, + value: PreferencesContextBase[T], + ) => void; +} export interface CoursePreferencesProps { color: string; diff --git a/src/core/hooks/useKeyboard.ts b/src/core/hooks/useKeyboardVisibile.ts similarity index 72% rename from src/core/hooks/useKeyboard.ts rename to src/core/hooks/useKeyboardVisibile.ts index 61402759..e02d7a8c 100644 --- a/src/core/hooks/useKeyboard.ts +++ b/src/core/hooks/useKeyboardVisibile.ts @@ -1,17 +1,17 @@ import { useEffect, useState } from 'react'; import { Keyboard, Platform } from 'react-native'; -export const useKeyboard = () => { +export const useKeyboardVisibile = () => { const [keyboardVisible, setKeyboardVisible] = useState(false); useEffect(() => { const showSubscription = Keyboard.addListener( - Platform.select({ ios: 'keyboardWillShow', android: 'keyboardDidShow' })!, + Platform.select({ ios: 'keyboardDidShow', android: 'keyboardDidShow' })!, () => { setKeyboardVisible(true); }, ); const hideSubscription = Keyboard.addListener( - Platform.select({ ios: 'keyboardWillHide', android: 'keyboardDidHide' })!, + Platform.select({ ios: 'keyboardDidHide', android: 'keyboardDidHide' })!, () => { setKeyboardVisible(false); }, diff --git a/src/core/providers/PreferencesProvider.tsx b/src/core/providers/PreferencesProvider.tsx index dd5c8059..0ba276b1 100644 --- a/src/core/providers/PreferencesProvider.tsx +++ b/src/core/providers/PreferencesProvider.tsx @@ -22,6 +22,7 @@ export const PreferencesProvider = ({ children }: PropsWithChildren) => { updatePreference: () => {}, favoriteServices: [], peopleSearched: [], + placesSearched: [], }); const preferencesInitialized = useRef(false); diff --git a/src/core/queries/courseHooks.ts b/src/core/queries/courseHooks.ts index a3c62700..0a513082 100644 --- a/src/core/queries/courseHooks.ts +++ b/src/core/queries/courseHooks.ts @@ -25,7 +25,6 @@ import { pluckData } from '../../utils/queries'; import { courseColors } from '../constants'; import { CoursesPreferences, - PreferenceKey, usePreferencesContext, } from '../contexts/PreferencesContext'; import { useGetExams } from './examHooks'; @@ -40,7 +39,9 @@ const useCoursesClient = (): CoursesApi => { const setupCourses = ( courses: CourseOverview[], coursePreferences: CoursesPreferences, - updatePreference: (key: PreferenceKey, value: unknown) => void, + updatePreference: ReturnType< + typeof usePreferencesContext + >['updatePreference'], ) => { let hasNewPreferences = false; // Associate each course with a set of preferences, if missing diff --git a/src/core/queries/placesHooks.ts b/src/core/queries/placesHooks.ts new file mode 100644 index 00000000..fc3dc9be --- /dev/null +++ b/src/core/queries/placesHooks.ts @@ -0,0 +1,158 @@ +import { useMemo } from 'react'; + +import { GetPlacesRequest, PlacesApi } from '@polito/api-client'; +import { GetFreeRoomsRequest } from '@polito/api-client/apis/PlacesApi'; +import { useQuery } from '@tanstack/react-query'; + +import { noop } from 'lodash'; + +export const SITES_QUERY_KEY = 'sites'; +export const BUILDINGS_QUERY_KEY = 'buildings'; +export const PLACES_QUERY_KEY = 'places'; +export const PLACE_QUERY_KEY = 'place'; +export const PLACE_CATEGORIES_QUERY_KEY = 'place-categories'; +export const FREE_ROOMS_QUERY_KEY = 'free-rooms'; + +const usePlacesClient = (): PlacesApi => { + return new PlacesApi(); +}; + +export const useGetSites = () => { + const placesClient = usePlacesClient(); + + return useQuery([SITES_QUERY_KEY], () => placesClient.getSites(), { + staleTime: Infinity, + }); +}; + +export const useGetBuildings = (siteId?: string) => { + const placesClient = usePlacesClient(); + + return useQuery( + [BUILDINGS_QUERY_KEY], + () => placesClient.getBuildings({ siteId: siteId! }), + { + staleTime: Infinity, + enabled: siteId != null, + }, + ); +}; + +export const useGetBuilding = (buildingId?: string) => { + const { data: buildings, ...rest } = useGetBuildings(); + return useMemo( + () => ({ + ...rest, + data: + buildingId == null || !buildings?.data?.length + ? null + : buildings.data.find(s => s.id === buildingId), + }), + [buildingId, buildings?.data, rest], + ); +}; + +export const useGetSite = (siteId?: string) => { + const { data: sites } = useGetSites(); + return useMemo(() => { + if (siteId == null || !sites?.data?.length) { + return null; + } + return sites.data.find(s => s.id === siteId); + }, [siteId, sites?.data]); +}; + +export const useGetPlaces = (params: GetPlacesRequest) => { + const placesClient = usePlacesClient(); + const key = [PLACES_QUERY_KEY]; + if (params.siteId != null) { + key.push(params.siteId); + } + if (params.search != null) { + key.push(params.search); + } + if (params.floorId != null) { + key.push(params.floorId); + } + if (params.buildingId != null) { + key.push(params.buildingId); + } + if (params.placeCategoryId != null) { + key.push(params.placeCategoryId); + } + if (params.placeSubCategoryId != null) { + key.push(params.placeSubCategoryId.join()); + } + + return useQuery(key, () => placesClient.getPlaces(params), { + enabled: params.siteId != null, + }); +}; + +export const useGetFreeRooms = (params: Partial) => { + const placesClient = usePlacesClient(); + const key = [ + FREE_ROOMS_QUERY_KEY, + params.siteId, + params.date, + params.timeFrom, + params.timeTo, + ]; + + return useQuery( + key, + () => placesClient.getFreeRooms(params as GetFreeRoomsRequest), + { + enabled: params.siteId != null, + staleTime: Infinity, + }, + ); +}; + +export const useGetPlaceCategories = () => { + const placesClient = usePlacesClient(); + + return useQuery( + [PLACE_CATEGORIES_QUERY_KEY], + () => placesClient.getPlaceCategories(), + { + staleTime: Infinity, + }, + ); +}; + +export const useGetPlaceCategory = (categoryId?: string) => { + const { data: categories } = useGetPlaceCategories(); + return useMemo(() => { + if (categoryId == null || !categories?.data?.length) { + return null; + } + return categories.data.find(c => c.id === categoryId); + }, [categoryId, categories?.data]); +}; + +export const useGetPlaceSubCategory = (subCategoryId?: string) => { + const { data: categories } = useGetPlaceCategories(); + return useMemo(() => { + if (subCategoryId == null || !categories?.data?.length) { + return null; + } + return categories.data + .flatMap(c => c.subCategories ?? []) + .find(sc => sc.id === subCategoryId); + }, [subCategoryId, categories?.data]); +}; + +export const useGetPlace = (placeId?: string) => { + const placesClient = usePlacesClient(); + + return useQuery( + [PLACE_QUERY_KEY, placeId], + () => placesClient.getPlace({ placeId: placeId! }), + { + enabled: placeId != null, + staleTime: Infinity, + onError: noop, + }, + ); +}; diff --git a/src/core/screens/LoginScreen.tsx b/src/core/screens/LoginScreen.tsx index 5a1f4cef..9ec5674d 100644 --- a/src/core/screens/LoginScreen.tsx +++ b/src/core/screens/LoginScreen.tsx @@ -42,7 +42,7 @@ export const LoginScreen = () => { const { language } = usePreferencesContext(); const handleLogin = () => - login({ username, password, preferences: { language } }).catch(e => { + login({ username, password, preferences: {} }).catch(e => { if (e instanceof UnsupportedUserTypeError) { Alert.alert(t('common.error'), t('loginScreen.unsupportedUserType')); } else { diff --git a/src/core/styles/globalStyles.ts b/src/core/styles/GlobalStyles.ts similarity index 100% rename from src/core/styles/globalStyles.ts rename to src/core/styles/GlobalStyles.ts diff --git a/src/features/agenda/components/AgendaTabs.tsx b/src/features/agenda/components/AgendaTabs.tsx new file mode 100644 index 00000000..c5ad930e --- /dev/null +++ b/src/features/agenda/components/AgendaTabs.tsx @@ -0,0 +1,120 @@ +import { useTranslation } from 'react-i18next'; +import { Platform, StyleSheet } from 'react-native'; + +import { Tab } from '@lib/ui/components/Tab'; +import { Tabs } from '@lib/ui/components/Tabs'; +import { useStylesheet } from '@lib/ui/hooks/useStylesheet'; +import { Theme } from '@lib/ui/types/Theme'; + +import { AgendaFiltersState } from '../types/AgendaFiltersState'; +import { AgendaItemType } from '../types/AgendaItem'; + +const activeTransparancyLight = '11'; +const activeTransparancyDark = '33'; + +interface Props { + state: AgendaFiltersState; + toggleState: (type: AgendaItemType) => void; +} + +export const AgendaTabs = ({ state, toggleState }: Props) => { + const { t } = useTranslation(); + + const styles = useStylesheet(createStyles); + + return ( + + toggleState('lecture')} + textStyle={state.lecture ? styles.tabText : styles.tabTextDisabled} + style={[ + styles.tab, + state.lecture ? styles.tabLecture : styles.tabDisabled, + ]} + > + {t('courseLecturesTab.title')} + + toggleState('exam')} + textStyle={state.exam ? styles.tabText : styles.tabTextDisabled} + style={[styles.tab, state.exam ? styles.tabExam : styles.tabDisabled]} + > + {t('examsScreen.title')} + + toggleState('booking')} + textStyle={state.booking ? styles.tabText : styles.tabTextDisabled} + style={[ + styles.tab, + state.booking ? styles.tabBooking : styles.tabDisabled, + ]} + > + {t('common.booking_plural')} + + toggleState('deadline')} + textStyle={state.deadline ? styles.tabText : styles.tabTextDisabled} + style={[ + styles.tab, + state.deadline ? styles.tabDeadline : styles.tabDisabled, + ]} + > + {t('common.deadline_plural')} + + + ); +}; + +const createStyles = ({ colors, palettes, dark }: Theme) => + StyleSheet.create({ + tabs: { + backgroundColor: colors.headersBackground, + borderBottomWidth: Platform.select({ + ios: StyleSheet.hairlineWidth, + }), + borderBottomColor: colors.divider, + elevation: 3, + zIndex: 1, + }, + tab: { + borderWidth: 1, + }, + tabText: { + color: colors.heading, + }, + tabTextDisabled: { + color: palettes.text[dark ? 400 : 500], + }, + tabDisabled: { + backgroundColor: 'transparent', + borderColor: palettes.text[dark ? 400 : 500], + }, + tabBooking: { + backgroundColor: + colors.agendaBooking + + (dark ? activeTransparancyDark : activeTransparancyLight), + borderColor: colors.agendaBooking, + }, + tabDeadline: { + backgroundColor: + colors.agendaDeadline + + (dark ? activeTransparancyDark : activeTransparancyLight), + borderColor: colors.agendaDeadline, + }, + tabExam: { + backgroundColor: + colors.agendaExam + + (dark ? activeTransparancyDark : activeTransparancyLight), + borderColor: colors.agendaExam, + }, + tabLecture: { + backgroundColor: + colors.agendaLecture + + (dark ? activeTransparancyDark : activeTransparancyLight), + borderColor: colors.agendaLecture, + }, + }); diff --git a/src/features/agenda/screens/LectureScreen.tsx b/src/features/agenda/screens/LectureScreen.tsx index 94a2e076..722a62af 100644 --- a/src/features/agenda/screens/LectureScreen.tsx +++ b/src/features/agenda/screens/LectureScreen.tsx @@ -16,8 +16,9 @@ import { EventDetails } from '../../../core/components/EventDetails'; import { VideoPlayer } from '../../../core/components/VideoPlayer'; import { useGetCourseVirtualClassrooms } from '../../../core/queries/courseHooks'; import { useGetPerson } from '../../../core/queries/peopleHooks'; -import { GlobalStyles } from '../../../core/styles/globalStyles'; +import { GlobalStyles } from '../../../core/styles/GlobalStyles'; import { convertMachineDateToFormatDate } from '../../../utils/dates'; +import { resolvePlaceId } from '../../places/utils/resolvePlaceId'; import { CourseIcon } from '../../teaching/components/CourseIcon'; import { isLiveVC, @@ -28,7 +29,7 @@ import { AgendaStackParamList } from '../components/AgendaNavigator'; type Props = NativeStackScreenProps; -export const LectureScreen = ({ route }: Props) => { +export const LectureScreen = ({ route, navigation }: Props) => { const { item: lecture } = route.params; const { t } = useTranslation(); const { fontSizes } = useTheme(); @@ -81,6 +82,17 @@ export const LectureScreen = ({ route }: Props) => { } title={lecture.place.name} + isAction + onPress={() => { + // @ts-expect-error Top-level navigation type + navigation.navigate('PlacesTab', { + screen: 'Place', + params: { + placeId: resolvePlaceId(lecture.place!), + }, + initial: false, + }); + }} /> )} {teacherQuery.data && ( diff --git a/src/features/agenda/types/AgendaFiltersState.ts b/src/features/agenda/types/AgendaFiltersState.ts new file mode 100644 index 00000000..5ac62f30 --- /dev/null +++ b/src/features/agenda/types/AgendaFiltersState.ts @@ -0,0 +1,3 @@ +import { AgendaItemType } from './AgendaItem'; + +export type AgendaFiltersState = Record; diff --git a/src/features/places/components/CampusSelector.tsx b/src/features/places/components/CampusSelector.tsx new file mode 100644 index 00000000..56a3e880 --- /dev/null +++ b/src/features/places/components/CampusSelector.tsx @@ -0,0 +1,51 @@ +import { useTranslation } from 'react-i18next'; + +import { faChevronDown, faSchool } from '@fortawesome/free-solid-svg-icons'; +import { Icon } from '@lib/ui/components/Icon'; +import { Row } from '@lib/ui/components/Row'; +import { Text } from '@lib/ui/components/Text'; +import { useTheme } from '@lib/ui/hooks/useTheme'; +import { MenuView } from '@react-native-menu/menu'; + +import { usePreferencesContext } from '../../../core/contexts/PreferencesContext'; +import { useGetSite, useGetSites } from '../../../core/queries/placesHooks'; + +export const CampusSelector = () => { + const { t } = useTranslation(); + const { spacing, colors, fontSizes } = useTheme(); + const { data: sites } = useGetSites(); + const { campusId, updatePreference } = usePreferencesContext(); + const campus = useGetSite(campusId); + + return ( + { + updatePreference( + 'campusId', + sites?.data?.find(s => s.id === newCampusId)?.id, + ); + }} + actions={ + sites?.data?.map(site => ({ + id: site.id, + title: site.name, + state: campusId === site.id ? 'on' : undefined, + })) ?? [] + } + > + + + + {campus?.name ?? t('common.campus')} + + + + + ); +}; diff --git a/src/features/places/components/IndoorMapLayer.tsx b/src/features/places/components/IndoorMapLayer.tsx new file mode 100644 index 00000000..1120ef10 --- /dev/null +++ b/src/features/places/components/IndoorMapLayer.tsx @@ -0,0 +1,41 @@ +import { useMemo } from 'react'; + +import { useTheme } from '@lib/ui/hooks/useTheme'; +import { RasterLayer, RasterSource } from '@rnmapbox/maps'; + +import { INTERIORS_MIN_ZOOM, MAX_ZOOM } from '../constants'; + +export interface IndoorMapLayerProps { + floorId?: string; +} + +export const IndoorMapLayer = ({ floorId }: IndoorMapLayerProps) => { + const { dark } = useTheme(); + const colorScheme = useMemo(() => (dark ? 'dark' : 'light'), [dark]); + const _floorId = floorId?.toLowerCase(); + + return ( + <> + + {_floorId && ( + + )} + + ); +}; diff --git a/src/features/places/components/MapControls.tsx b/src/features/places/components/MapControls.tsx new file mode 100644 index 00000000..24fa3565 --- /dev/null +++ b/src/features/places/components/MapControls.tsx @@ -0,0 +1,82 @@ +import { useTranslation } from 'react-i18next'; +import { StyleSheet, TouchableOpacity } from 'react-native'; + +import { + faCrosshairs, + faElevator, + faExpand, +} from '@fortawesome/free-solid-svg-icons'; +import { Divider } from '@lib/ui/components/Divider'; +import { Icon } from '@lib/ui/components/Icon'; +import { IconButton } from '@lib/ui/components/IconButton'; +import { Row } from '@lib/ui/components/Row'; +import { Text } from '@lib/ui/components/Text'; +import { TranslucentCard } from '@lib/ui/components/TranslucentCard'; +import { useStylesheet } from '@lib/ui/hooks/useStylesheet'; +import { useTheme } from '@lib/ui/hooks/useTheme'; +import { Theme } from '@lib/ui/types/Theme'; +import { MenuView } from '@react-native-menu/menu'; + +export const MapControls = () => { + const { t } = useTranslation(); + const styles = useStylesheet(createStyles); + const { colors } = useTheme(); + + return ( + + + + + + + + + + + + + 0 + + + + + + ); +}; + +const createStyles = ({ spacing }: Theme) => + StyleSheet.create({ + container: { + position: 'absolute', + bottom: 80, + left: spacing[5], + right: spacing[5], + }, + divider: { + alignSelf: 'stretch', + }, + icon: { + paddingHorizontal: spacing[3], + paddingVertical: spacing[2.5], + }, + }); diff --git a/src/features/places/components/MapNavigator.tsx b/src/features/places/components/MapNavigator.tsx new file mode 100644 index 00000000..b99769c8 --- /dev/null +++ b/src/features/places/components/MapNavigator.tsx @@ -0,0 +1,317 @@ +import { + ComponentProps, + ReactNode, + useContext, + useEffect, + useRef, + useState, +} from 'react'; +import { + ActivityIndicator, + Image, + SafeAreaView, + StyleSheet, + View, +} from 'react-native'; +import { NativeStackNavigatorProps } from 'react-native-screens/lib/typescript/native-stack/types'; + +import { useBottomTabBarHeight } from '@react-navigation/bottom-tabs'; +import { + Header, + HeaderBackButton, + HeaderBackContext, + Screen, + getHeaderTitle, + useHeaderHeight, +} from '@react-navigation/elements'; +import { + DefaultRouterOptions, + NavigationProp, + ParamListBase, + RouteProp, + StackActionHelpers, + StackNavigationState, + StackRouter, + createNavigatorFactory, + useNavigationBuilder, +} from '@react-navigation/native'; +import { + NativeStackNavigationEventMap, + NativeStackNavigationOptions, +} from '@react-navigation/native-stack'; +import { Camera, MapView } from '@rnmapbox/maps'; +import { CameraProps } from '@rnmapbox/maps/lib/typescript/components/Camera'; + +import { IS_ANDROID, IS_IOS } from '../../../core/constants'; +import { useDeviceOrientation } from '../../../core/hooks/useDeviceOrientation'; +import { useKeyboardVisibile } from '../../../core/hooks/useKeyboardVisibile'; +import { GlobalStyles } from '../../../core/styles/GlobalStyles'; +import { MapNavigatorContext } from '../contexts/MapNavigatorContext'; + +interface Insets { + top?: number; + bottom?: number; + left?: number; + right?: number; +} + +interface RouteProps { + renderRoute: (...args: unknown[]) => ReactNode; +} + +const Route = ({ renderRoute }: RouteProps) => { + const headerHeight = useHeaderHeight(); + const keyboardVisible = useKeyboardVisibile(); + const tabBarHeight = useBottomTabBarHeight(); + return ( + + {renderRoute()} + + ); +}; + +export const MapNavigator = ({ + initialRouteName, + children, + screenOptions, +}: NativeStackNavigatorProps) => { + const { state, navigation, descriptors, NavigationContent } = + useNavigationBuilder< + StackNavigationState, + DefaultRouterOptions, + StackActionHelpers, + MapNavigationOptions, + NativeStackNavigationEventMap + >(StackRouter, { + children, + screenOptions, + initialRouteName, + }); + const mapRef = useRef(null); + const cameraRef = useRef(null); + const currentRoute = descriptors[state.routes[state.index].key]; + const mapDefaultOptions = currentRoute.options?.mapDefaultOptions ?? {}; + const mapOptions = currentRoute.options?.mapOptions ?? {}; + const previousKey = state.routes[state.index - 1]?.key; + const previousDescriptor = previousKey ? descriptors[previousKey] : undefined; + const parentHeaderBack = useContext(HeaderBackContext); + const headerBack = previousDescriptor + ? { + title: getHeaderTitle( + previousDescriptor.options, + previousDescriptor.route.name, + ), + } + : parentHeaderBack; + const canGoBack = headerBack !== undefined; + const title = getHeaderTitle( + currentRoute.options, + state.routes[state.index].name, + ); + const orientation = useDeviceOrientation(); + const [rotating, setRotating] = useState(false); + + useEffect(() => { + if (IS_IOS) { + setRotating(true); + setTimeout(() => { + setRotating(false); + }, 1500); + } + }, [orientation]); + + const { + header, + headerShown, + headerTintColor, + headerBackImageSource, + headerLeft, + headerRight, + headerTitle, + headerTitleAlign, + headerTitleStyle, + headerStyle, + headerShadowVisible, + headerTransparent, + headerBackground, + headerBackTitle, + headerBackTitleStyle, + headerBackVisible, + headerBackTitleVisible, + } = currentRoute.options; + + return ( + + + headerLeft({ + tintColor, + canGoBack, + label: headerBackTitle, + }) + : headerLeft === undefined && canGoBack + ? ({ tintColor }) => ( + ( + + ) + : undefined + } + onPress={navigation.goBack} + canGoBack={canGoBack} + label={ + headerBackTitle ?? previousDescriptor?.options.title + } + labelVisible={IS_IOS && headerBackTitleVisible} + labelStyle={headerBackTitleStyle} + /> + ) + : headerLeft + : undefined + } + headerRight={ + typeof headerRight === 'function' + ? ({ tintColor }) => headerRight({ tintColor, canGoBack }) + : headerRight + } + headerTitle={ + typeof headerTitle === 'function' + ? ({ children: titleChildren, tintColor }) => + headerTitle({ children: titleChildren, tintColor }) + : headerTitle + } + headerTitleAlign={headerTitleAlign} + headerTitleStyle={headerTitleStyle} + headerTransparent={headerTransparent} + headerShadowVisible={headerShadowVisible} + headerBackground={headerBackground} + headerStyle={headerStyle} + /> + ) + } + > + + + + {currentRoute.options?.mapDefaultContent} + {currentRoute.options?.mapContent} + + + + {!rotating ? ( + + ) : ( + + + + )} + + + + + ); +}; + +const styles = StyleSheet.create({ + backImage: { + height: 24, + width: 24, + margin: 3, + resizeMode: 'contain', + }, +}); + +type MapViewProps = ComponentProps; + +type MapOptions = Partial< + Omit & { + camera: Partial; + insets?: Insets; + } +>; + +export type MapNavigationOptions = NativeStackNavigationOptions & { + mapOptions?: MapOptions; + mapDefaultOptions?: MapOptions; + mapContent?: JSX.Element; + mapDefaultContent?: JSX.Element; +}; + +export const createMapNavigator = createNavigatorFactory< + StackNavigationState, + MapNavigationOptions, + NativeStackNavigationEventMap, + typeof MapNavigator +>(MapNavigator); + +export type MapNavigationProp< + ParamList extends ParamListBase, + RouteName extends keyof ParamList = string, + NavigatorID extends string | undefined = undefined, +> = NavigationProp< + ParamList, + RouteName, + NavigatorID, + StackNavigationState, + MapNavigationOptions, + NativeStackNavigationEventMap +> & + StackActionHelpers; + +export type MapScreenProps< + ParamList extends ParamListBase, + RouteName extends keyof ParamList = string, + NavigatorID extends string | undefined = undefined, +> = { + navigation: MapNavigationProp; + route: RouteProp; +}; diff --git a/src/features/places/components/MarkersLayer.tsx b/src/features/places/components/MarkersLayer.tsx new file mode 100644 index 00000000..bff843e4 --- /dev/null +++ b/src/features/places/components/MarkersLayer.tsx @@ -0,0 +1,181 @@ +/* eslint-disable @typescript-eslint/naming-convention */ +import { useMemo } from 'react'; +import { useTranslation } from 'react-i18next'; + +import { useTheme } from '@lib/ui/hooks/useTheme'; +import { Theme } from '@lib/ui/types/Theme'; +import { useNavigation } from '@react-navigation/native'; +import { Images, ShapeSource, SymbolLayer } from '@rnmapbox/maps'; + +import { CATEGORIES_DATA, MARKERS_MIN_ZOOM } from '../constants'; +import { + CategoryData, + PlaceOverviewWithMetadata, + SearchPlace, + isPlace, +} from '../types'; +import { formatAgendaItem } from '../utils/formatAgendaItem'; +import { MapScreenProps } from './MapNavigator'; +import { PlacesStackParamList } from './PlacesNavigator'; + +export interface MarkersLayerProps { + selectedPoiId?: string; + search?: string; + places?: SearchPlace[]; + displayFloor?: boolean; +} + +export const MarkersLayer = ({ + selectedPoiId, + places = [], + displayFloor, +}: MarkersLayerProps) => { + const { navigate } = + useNavigation['navigation']>(); + const { t } = useTranslation(); + const { dark, fontSizes, palettes } = useTheme(); + const pois = useMemo((): (SearchPlace & CategoryData)[] => { + return ( + places + // ?.filter(p => { + // const { + // id: catId, + // subCategory: { id: subCatId }, + // } = p.category; + // return ( + // CATEGORIES_DATA[catId]?.children[subCatId] != null || + // selectedPoiId === p.id + // ); + // }) + ?.map(poi => { + const categoryData = (poi as PlaceOverviewWithMetadata).category?.id + ? CATEGORIES_DATA[ + (poi as PlaceOverviewWithMetadata).category + .id as keyof typeof CATEGORIES_DATA + ] ?? CATEGORIES_DATA.default + : CATEGORIES_DATA.default; + const subcategoryData = (poi as PlaceOverviewWithMetadata).category + ?.subCategory?.id + ? (categoryData.children[ + (poi as PlaceOverviewWithMetadata).category.subCategory + .id as keyof typeof categoryData.children + ] as any) ?? {} + : {}; + + const markerData = { + ...poi, + ...categoryData, + ...subcategoryData, + priority: + selectedPoiId === poi.id || + (poi as PlaceOverviewWithMetadata).agendaItem != null + ? 0 + : subcategoryData?.priority ?? categoryData.priority, + }; + if (!markerData.icon) { + markerData.icon = 'pin'; + markerData.color = 'gray'; + } + return markerData; + }) + ); + }, [places, selectedPoiId]); + + return ( + <> + + {pois && ( + { + return { + type: 'Feature', + id: `poi-point-${p.id}`, + properties: { + dark, + index: i, + icon: p.icon, + priority: p.priority, + name: isPlace(p) + ? `${p.room.name ?? p.category.subCategory.name}${ + p.agendaItem != null + ? `\n${formatAgendaItem(p.agendaItem, true)}` + : displayFloor + ? `\n${t('common.floor')} ${p.floor.level}` + : '' + }` + : p.name, + color: + palettes[p.color as keyof Theme['palettes']][ + dark ? 200 : p.shade ?? 500 + ], + }, + geometry: { + type: 'Point', + coordinates: [p.longitude, p.latitude], + }, + }; + }), + }} + existing={false} + onPress={({ features }) => { + const selectedPoi = features?.[0] + ? pois?.[features[0].properties?.index] + : null; + if (selectedPoi) { + navigate({ + name: 'Place', + params: { placeId: selectedPoi.id }, + }); + } + }} + > + + + )} + + ); +}; diff --git a/src/features/places/components/PlaceCategoriesBottomSheet.tsx b/src/features/places/components/PlaceCategoriesBottomSheet.tsx new file mode 100644 index 00000000..5881ff37 --- /dev/null +++ b/src/features/places/components/PlaceCategoriesBottomSheet.tsx @@ -0,0 +1,72 @@ +import { useRef, useState } from 'react'; +import { useTranslation } from 'react-i18next'; + +import { faMapLocation } from '@fortawesome/free-solid-svg-icons'; +import { BottomSheetMethods } from '@gorhom/bottom-sheet/lib/typescript/types'; +import { EmptyState } from '@lib/ui/components/EmptyState'; +import { Icon } from '@lib/ui/components/Icon'; +import { useTheme } from '@lib/ui/hooks/useTheme'; +import { NavigationProp, useNavigation } from '@react-navigation/native'; + +import { useGetPlaceCategories } from '../../../core/queries/placesHooks'; +import { formatPlaceCategory } from '../utils/category'; +import { PlacesBottomSheet, PlacesBottomSheetProps } from './PlacesBottomSheet'; +import { PlacesStackParamList } from './PlacesNavigator'; + +export const PlaceCategoriesBottomSheet = (props: PlacesBottomSheetProps) => { + const navigation = + useNavigation>(); + const { t } = useTranslation(); + const { fontSizes } = useTheme(); + const sheetRef = useRef(null); + const { data: categories, isLoading } = useGetPlaceCategories(); + const [search, setSearch] = useState(''); + + return ( + + c.subCategories.map(sc => ({ + ...sc, + parent: formatPlaceCategory(c.name), + })), + ) + .filter( + sc => + !search || sc.name.toLowerCase().includes(search.toLowerCase()), + ) + .map(sc => ({ + title: sc.name, + subtitle: sc.parent, + isAction: true, + leadingItem: , + onPress: () => { + sheetRef.current?.close(); + navigation.navigate({ + name: 'Places', + key: `Places:${sc.id}`, + params: { + subCategoryId: sc.id, + }, + }); + }, + })), + ListEmptyComponent: ( + + ), + }} + {...props} + /> + ); +}; diff --git a/src/features/places/components/PlaceMarker.tsx b/src/features/places/components/PlaceMarker.tsx new file mode 100644 index 00000000..d913da2f --- /dev/null +++ b/src/features/places/components/PlaceMarker.tsx @@ -0,0 +1,22 @@ +import { MarkerView } from '@rnmapbox/maps'; + +import { useGetPlace } from '../../../core/queries/placesHooks'; + +interface Props { + placeId: string; +} + +export const PlaceMarker = ({ placeId }: Props) => { + const { data: place, isLoading } = useGetPlace(placeId); + + if (isLoading || !place?.data) { + return null; + } + return ( + + ); +}; diff --git a/src/features/places/components/PlacePanel.tsx b/src/features/places/components/PlacePanel.tsx new file mode 100644 index 00000000..bd3598cf --- /dev/null +++ b/src/features/places/components/PlacePanel.tsx @@ -0,0 +1,26 @@ +import { View } from 'react-native'; + +import { Text } from '@lib/ui/components/Text'; +import { useTheme } from '@lib/ui/hooks/useTheme'; + +interface Props { + placeId: string; +} + +export const PlacePanel = ({ placeId }: Props) => { + const { spacing, fontSizes } = useTheme(); + + return ( + + + Aula 1 + + + Aula + + + Corso Duca degli Abruzzi, 24, 10129, Torino + + + ); +}; diff --git a/src/features/places/components/PlacesBottomSheet.tsx b/src/features/places/components/PlacesBottomSheet.tsx new file mode 100644 index 00000000..801c54c9 --- /dev/null +++ b/src/features/places/components/PlacesBottomSheet.tsx @@ -0,0 +1,98 @@ +import { forwardRef, useImperativeHandle, useRef, useState } from 'react'; +import { useTranslation } from 'react-i18next'; + +import { faMapPin } from '@fortawesome/free-solid-svg-icons'; +import { BottomSheetFlatList } from '@gorhom/bottom-sheet'; +import { BottomSheetFlatListProps } from '@gorhom/bottom-sheet/lib/typescript/components/bottomSheetScrollable/types'; +import { BottomSheetMethods } from '@gorhom/bottom-sheet/lib/typescript/types'; +import { ActivityIndicator } from '@lib/ui/components/ActivityIndicator'; +import { BottomSheet, BottomSheetProps } from '@lib/ui/components/BottomSheet'; +import { BottomSheetTextField } from '@lib/ui/components/BottomSheetTextField'; +import { Icon } from '@lib/ui/components/Icon'; +import { IndentedDivider } from '@lib/ui/components/IndentedDivider'; +import { ListItem, ListItemProps } from '@lib/ui/components/ListItem'; +import { TranslucentTextFieldProps } from '@lib/ui/components/TranslucentTextField'; +import { useTheme } from '@lib/ui/hooks/useTheme'; + +export interface PlacesBottomSheetProps + extends Omit { + textFieldProps?: Partial; + searchFieldLabel?: string; + listProps?: Partial>; + isLoading?: boolean; + search?: string; + onSearchChange?: (newSearch: string) => void; +} + +export const PlacesBottomSheet = forwardRef< + BottomSheetMethods, + PlacesBottomSheetProps +>( + ( + { + textFieldProps, + listProps, + searchFieldLabel, + isLoading = false, + search, + onSearchChange, + ...props + }, + ref, + ) => { + const { t } = useTranslation(); + const { fontSizes, spacing } = useTheme(); + const innerRef = useRef(null); + const [typing, setTyping] = useState(false); + const [recentSearches, setRecentSearches] = useState([]); + + useImperativeHandle(ref, () => innerRef.current!); + + return ( + + { + setTyping(true); + innerRef.current?.expand(); + }} + onBlur={() => { + setTyping(false); + innerRef.current?.snapToIndex(1); + }} + value={search} + onChangeText={onSearchChange} + {...textFieldProps} + /> + ( + } + {...item} + title={item.title ?? t('common.untitled')} + /> + )} + ItemSeparatorComponent={IndentedDivider} + {...listProps} + data={ + typing && !(listProps?.data as any[])?.length + ? recentSearches + : listProps?.data ?? [] + } + ListEmptyComponent={ + isLoading ? ( + + ) : ( + listProps?.ListEmptyComponent + ) + } + /> + + ); + }, +); diff --git a/src/features/places/components/PlacesMarkers.tsx b/src/features/places/components/PlacesMarkers.tsx new file mode 100644 index 00000000..caa25b9a --- /dev/null +++ b/src/features/places/components/PlacesMarkers.tsx @@ -0,0 +1,95 @@ +import { Text } from '@lib/ui/components/Text'; +import { RouteProp, useNavigation, useRoute } from '@react-navigation/native'; +import { StackNavigationProp } from '@react-navigation/stack'; +import { MarkerView } from '@rnmapbox/maps'; + +import { useGetPlaces } from '../../../core/queries/placesHooks'; +import { PlacesStackParamList } from './PlacesNavigator'; + +interface Props { + search?: string; + siteId?: string; + placeType?: string; +} + +export const PlacesMarkers = ({ + placeType, + search = 'aula', + siteId, +}: Props) => { + const { navigate } = + useNavigation>(); + const { params } = useRoute>(); + const { data: places, isLoading } = useGetPlaces({ + search, + siteId, + }); + + if (isLoading || !places?.data.length) { + return null; + } + + return ( + <> + + + + Aula 1 + + + + + + + Aula 2 + + + + + + + Aula 4 + + + {/* {places.data.map(place => {*/} + {/* return (*/} + {/* */} + {/* );*/} + {/* })}*/} + + ); +}; diff --git a/src/features/places/components/PlacesNavigator.tsx b/src/features/places/components/PlacesNavigator.tsx index 8cb5294e..0bdaaa73 100644 --- a/src/features/places/components/PlacesNavigator.tsx +++ b/src/features/places/components/PlacesNavigator.tsx @@ -1,44 +1,130 @@ +/* eslint-disable @typescript-eslint/naming-convention */ +import { useEffect, useMemo } from 'react'; import { useTranslation } from 'react-i18next'; -import { Platform } from 'react-native'; +import { StyleSheet, View } from 'react-native'; +import { PERMISSIONS, request } from 'react-native-permissions'; +import { Divider } from '@lib/ui/components/Divider'; import { useTheme } from '@lib/ui/hooks/useTheme'; import { createNativeStackNavigator } from '@react-navigation/native-stack'; +import { + MapState, + RasterLayer, + RasterSource, + UserLocation, +} from '@rnmapbox/maps'; import { HeaderCloseButton } from '../../../core/components/HeaderCloseButton'; import { HeaderLogo } from '../../../core/components/HeaderLogo'; +import { TranslucentView } from '../../../core/components/TranslucentView'; import { useTitlesStyles } from '../../../core/hooks/useTitlesStyles'; import { UnreadMessagesModal } from '../../user/screens/UnreadMessagesModal'; +import { MAX_ZOOM } from '../constants'; +import { BuildingScreen } from '../screens/BuildingScreen'; +import { FreeRoomsScreen } from '../screens/FreeRoomsScreen'; +import { PlaceScreen } from '../screens/PlaceScreen'; import { PlacesScreen } from '../screens/PlacesScreen'; +import { createMapNavigator } from './MapNavigator'; export type ServiceStackParamList = { Places: undefined; MessagesModal: undefined; }; - const Stack = createNativeStackNavigator(); +export type PlacesStackParamList = { + Places: { + categoryId?: string; + subCategoryId?: string; + pitch?: number; + bounds?: MapState['properties']['bounds']; + }; + Place: { + placeId: string; + }; + Building: { + buildingId: string; + }; + PlaceCategories: undefined; + MessagesModal: undefined; + FreeRooms: undefined; +}; + +const Map = createMapNavigator(); + export const PlacesNavigator = () => { const { t } = useTranslation(); const theme = useTheme(); - const { colors } = theme; + const colorScheme = useMemo(() => (theme.dark ? 'dark' : 'light'), [theme]); + + useEffect(() => { + request(PERMISSIONS.IOS.LOCATION_WHEN_IN_USE); + request(PERMISSIONS.ANDROID.ACCESS_FINE_LOCATION); + }, []); return ( - ( + + + + + ), + mapDefaultOptions: { + camera: { + animationDuration: 2000, + animationMode: 'flyTo', + maxZoomLevel: MAX_ZOOM, + }, + attributionEnabled: false, + compassEnabled: true, + compassFadeWhenNorth: true, + styleJSON: + '{"version":8,"sources":{},"layers":[],"glyphs":"mapbox://fonts/mapbox/{fontstack}/{range}.pbf"}', }, + mapDefaultContent: ( + <> + + + + + + ), ...useTitlesStyles(theme), }} > - + , - headerTitle: t('placesScreen.title'), + title: t('placeScreen.title'), + }} + /> + { headerRight: () => , }} /> - + + ); }; diff --git a/src/features/places/constants.ts b/src/features/places/constants.ts new file mode 100644 index 00000000..43efbe7b --- /dev/null +++ b/src/features/places/constants.ts @@ -0,0 +1,141 @@ +/* eslint-disable @typescript-eslint/naming-convention */ +import { CategoryData } from './types'; + +export const INTERIORS_MIN_ZOOM = 18.6; +export const MAX_ZOOM = 24; +export const MARKERS_MIN_ZOOM = 15; +export const CATEGORIES_DATA: Record = { + default: { + icon: 'pin', + color: 'gray', + priority: 100, + children: {}, + }, + VERT: { + icon: 'stairs', + color: 'gray', + priority: 100, + children: { + SCALA: {}, + }, + }, + PARK: { + icon: 'car', + color: 'gray', + priority: 100, + children: {}, + }, + RESIDENCE: { + icon: 'bed', + color: 'red', + priority: 100, + children: {}, + }, + SERV: { + icon: 'pin', + color: 'gray', + priority: 90, + children: { + 'PUNTO H2O': { icon: 'water', color: 'lightBlue', priority: 20 }, + WC: { icon: 'restroom', color: 'green', shade: 600 }, + WC_F: { icon: 'restroom', color: 'green', shade: 600 }, + WC_H: { icon: 'restroom', color: 'green', shade: 600 }, + WC_M: { icon: 'restroom', color: 'green', shade: 600 }, + }, + }, + SUPP: { + icon: 'pin', + color: 'gray', + priority: 100, + children: { + S_CONFEREN: { icon: 'conference' }, + }, + }, + UFF: { + icon: 'pin', + color: 'gray', + priority: 60, + children: {}, + }, + AULA: { + icon: 'classroom', + color: 'navy', + priority: 40, + children: { + AULA: {}, + AULA_DIS: {}, + AULA_INF: {}, + AULA_LAB: {}, + }, + }, + LAIB: { + icon: 'lab', + color: 'navy', + priority: 60, + children: {}, + }, + INGRESSO: { + icon: 'door', + color: 'gray', + priority: 60, + children: {}, + }, + UFF_PUBB: { + icon: 'office', + color: 'gray', + priority: 60, + children: {}, + }, + LAB: { + icon: 'classroom', + color: 'navy', + priority: 60, + children: {}, + }, + STUD: { + icon: 'study', + color: 'navy', + priority: 40, + children: { + BIBLIO: { icon: 'library' }, + STUD_EST_A: {}, + STUD_EST_P: {}, + S_STUD: {}, + }, + }, + TECN: { + icon: 'pin', + color: 'gray', + priority: 100, + children: {}, + }, + SPEC: { + icon: 'service', + color: 'red', + priority: 60, + children: { + BAR: { icon: 'bar' }, + SALA_BAR: { icon: 'bar' }, + MENSA: { icon: 'restaurant' }, + RISTORA: { icon: 'restaurant' }, + Z_RIST: { icon: 'restaurant' }, + INFERM: { icon: 'medical' }, + POSTA: { icon: 'post' }, + }, + }, + TBD: { + icon: 'pin', + color: 'gray', + priority: 100, + children: {}, + }, + EST: { + icon: 'pin', + color: 'gray', + priority: 100, + children: { + PARK_BIKE: { icon: 'bike' }, + }, + }, +}; +export const UPCOMING_COMMITMENT_HOURS_OFFSET = 24; diff --git a/src/features/places/contexts/MapNavigatorContext.ts b/src/features/places/contexts/MapNavigatorContext.ts new file mode 100644 index 00000000..bd3f5e64 --- /dev/null +++ b/src/features/places/contexts/MapNavigatorContext.ts @@ -0,0 +1,12 @@ +import { RefObject, createContext } from 'react'; + +import { Camera, MapView } from '@rnmapbox/maps'; + +interface MapNavigatorContextValue { + mapRef: RefObject; + cameraRef: RefObject; +} + +export const MapNavigatorContext = createContext( + {} as MapNavigatorContextValue, +); diff --git a/src/features/places/hooks/useGetCurrentCampus.ts b/src/features/places/hooks/useGetCurrentCampus.ts new file mode 100644 index 00000000..38debe4e --- /dev/null +++ b/src/features/places/hooks/useGetCurrentCampus.ts @@ -0,0 +1,7 @@ +import { usePreferencesContext } from '../../../core/contexts/PreferencesContext'; +import { useGetSite } from '../../../core/queries/placesHooks'; + +export const useGetCurrentCampus = () => { + const { campusId } = usePreferencesContext(); + return useGetSite(campusId); +}; diff --git a/src/features/places/hooks/useSearchPlaces.ts b/src/features/places/hooks/useSearchPlaces.ts new file mode 100644 index 00000000..78b7e5b3 --- /dev/null +++ b/src/features/places/hooks/useSearchPlaces.ts @@ -0,0 +1,131 @@ +import { useMemo, useRef } from 'react'; + +import { DateTime } from 'luxon'; + +import { usePreferencesContext } from '../../../core/contexts/PreferencesContext'; +import { + useGetBuildings, + useGetPlaces, +} from '../../../core/queries/placesHooks'; +import { useGetAgendaWeeks } from '../../agenda/queries/agendaHooks'; +import { LectureItem } from '../../agenda/types/AgendaItem'; +import { UPCOMING_COMMITMENT_HOURS_OFFSET } from '../constants'; +import { + BuildingWithMetadata, + PlaceOverviewWithMetadata, + isPlace, +} from '../types'; +import { resolvePlaceId } from '../utils/resolvePlaceId'; +import { useGetCurrentCampus } from './useGetCurrentCampus'; + +interface UseSearchPlacesOptions { + search?: string; + siteId?: string; + floorId?: string; + categoryId?: string; + subCategoryId?: string; +} + +export const useSearchPlaces = ({ + siteId, + search, + floorId, + categoryId, + subCategoryId, +}: UseSearchPlacesOptions) => { + const { courses: coursesPreferences, placesSearched } = + usePreferencesContext(); + + const campus = useGetCurrentCampus(); + const actualSiteId = siteId ?? campus?.id; + + const now = useRef(DateTime.now()); + const { data: agendaPages } = useGetAgendaWeeks( + coursesPreferences, + now.current, + ); + const upcomingCommitments = useMemo( + () => + agendaPages?.pages?.[0]?.data + .flatMap(i => i.items) + .filter( + i => + (i as LectureItem).place != null && + ((i.start >= now.current && + i.start.diff(now.current).milliseconds < + UPCOMING_COMMITMENT_HOURS_OFFSET * 60 * 60 * 1000) || + (i.start <= now.current && i.end >= now.current)), + ) as + | (LectureItem & { place: Exclude })[] + | undefined, + [agendaPages], + ); + + const { data: places, fetchStatus: placesFetchStatus } = useGetPlaces({ + search: search || undefined, + siteId: actualSiteId, + floorId: search?.length ? undefined : floorId, + placeCategoryId: categoryId, + placeSubCategoryId: subCategoryId ? [subCategoryId] : undefined, + }); + + const { data: buildings, fetchStatus: buildingFetchStatus } = + useGetBuildings(actualSiteId); + + const combinedPlaces = useMemo(() => { + let result = places?.data + ?.map(p => ({ ...p, type: 'place' })) + ?.sort(a => (placesSearched.some(p => a.id === p.id) ? -1 : 1)) as + | (PlaceOverviewWithMetadata | BuildingWithMetadata)[] + | undefined; + if (!upcomingCommitments?.length || !result?.length) { + return result; + } + for (const commitment of upcomingCommitments.reverse()) { + const placeIndex = result.findIndex( + p => p.id === resolvePlaceId(commitment.place), + ); + if (placeIndex !== -1) { + const place = result.splice( + placeIndex, + 1, + )[0] as PlaceOverviewWithMetadata; + place.agendaItem = commitment; + result.unshift(place); + } + } + if (buildings?.data?.length) { + result = result?.concat( + buildings.data.map(b => ({ + ...b, + type: 'building', + })), + ); + } + if (search) { + result = result?.filter(p => { + if (isPlace(p)) { + return ( + p.room.name.toLowerCase().includes(search) || + p.category.name.toLowerCase().includes(search) || + p.category.subCategory.name.toLowerCase().includes(search) + ); + } + return p.name.toLowerCase().includes(search); + }); + } + return result?.filter(p => p.latitude != null && p.longitude != null); + }, [ + buildings?.data, + places?.data, + placesSearched, + search, + upcomingCommitments, + ]); + + return { + places: combinedPlaces, + isLoading: + placesFetchStatus === 'fetching' || buildingFetchStatus === 'fetching', + }; +}; diff --git a/src/features/places/screens/BuildingScreen.tsx b/src/features/places/screens/BuildingScreen.tsx new file mode 100644 index 00000000..b8705ef9 --- /dev/null +++ b/src/features/places/screens/BuildingScreen.tsx @@ -0,0 +1,240 @@ +import { useLayoutEffect } from 'react'; +import { useTranslation } from 'react-i18next'; +import { Dimensions, Linking, Platform, StyleSheet, View } from 'react-native'; +import { useSafeAreaInsets } from 'react-native-safe-area-context'; + +import { + faDiamondTurnRight, + faSignsPost, +} from '@fortawesome/free-solid-svg-icons'; +import { BottomSheetScrollView } from '@gorhom/bottom-sheet'; +import { ActivityIndicator } from '@lib/ui/components/ActivityIndicator'; +import { BottomSheet } from '@lib/ui/components/BottomSheet'; +import { Col } from '@lib/ui/components/Col'; +import { EmptyState } from '@lib/ui/components/EmptyState'; +import { IconButton } from '@lib/ui/components/IconButton'; +import { ListItem } from '@lib/ui/components/ListItem'; +import { OverviewList } from '@lib/ui/components/OverviewList'; +import { Section } from '@lib/ui/components/Section'; +import { SectionHeader } from '@lib/ui/components/SectionHeader'; +import { Text } from '@lib/ui/components/Text'; +import { useStylesheet } from '@lib/ui/hooks/useStylesheet'; +import { useTheme } from '@lib/ui/hooks/useTheme'; +import { Theme } from '@lib/ui/types/Theme'; +import { ResponseError } from '@polito/api-client/runtime'; +import { useHeaderHeight } from '@react-navigation/elements'; +import { FillLayer, LineLayer, ShapeSource } from '@rnmapbox/maps'; + +import { IS_IOS } from '../../../core/constants'; +import { usePreferencesContext } from '../../../core/contexts/PreferencesContext'; +import { useScreenTitle } from '../../../core/hooks/useScreenTitle'; +import { useGetBuilding } from '../../../core/queries/placesHooks'; +import { GlobalStyles } from '../../../core/styles/GlobalStyles'; +import { IndoorMapLayer } from '../components/IndoorMapLayer'; +import { MapScreenProps } from '../components/MapNavigator'; +import { MarkersLayer } from '../components/MarkersLayer'; +import { PlacesStackParamList } from '../components/PlacesNavigator'; +import { useSearchPlaces } from '../hooks/useSearchPlaces'; +import { formatPlaceCategory } from '../utils/category'; + +type Props = MapScreenProps; + +export const BuildingScreen = ({ navigation, route }: Props) => { + const { palettes } = useTheme(); + const styles = useStylesheet(createStyles); + const { t } = useTranslation(); + const { placesSearched, updatePreference } = usePreferencesContext(); + const { fontSizes, spacing } = useTheme(); + const headerHeight = useHeaderHeight(); + const safeAreaInsets = useSafeAreaInsets(); + const { buildingId } = route.params; + const { + data: building, + isLoading: isLoadingBuilding, + error: getPlaceError, + } = useGetBuilding(buildingId); + const siteId = building?.site.id; + const floorId = building?.floor.id; + const { places, isLoading: isLoadingPlaces } = useSearchPlaces({ + siteId, + floorId, + }); + const isLoading = isLoadingBuilding || isLoadingPlaces; + const placeName = + building?.name ?? + building?.category.subCategory.name ?? + t('common.untitled'); + + useScreenTitle( + (getPlaceError as ResponseError)?.response?.status === 404 + ? t('common.notFound') + : placeName, + ); + + useLayoutEffect(() => { + if (building) { + const { latitude, longitude } = building; + navigation.setOptions({ + mapOptions: { + compassPosition: IS_IOS + ? { + top: headerHeight - safeAreaInsets.top + spacing[2], + right: spacing[3], + } + : undefined, + camera: { + centerCoordinate: [longitude, latitude], + padding: { + paddingTop: 0, + paddingLeft: 0, + paddingRight: 0, + paddingBottom: Dimensions.get('window').height / 2 - headerHeight, + }, + zoomLevel: 19, + }, + }, + mapContent: ( + <> + + + {building?.geoJson != null && ( + + + + + )} + + ), + }); + } + }, [ + buildingId, + building, + navigation, + headerHeight, + palettes.secondary, + floorId, + safeAreaInsets.top, + spacing, + updatePreference, + placesSearched, + places, + ]); + + if (isLoading) { + return ( + + + + + + ); + } + + if ( + getPlaceError && + (getPlaceError as ResponseError).response.status === 404 + ) { + return ( + + + + + + ); + } + + if (!building) { + return null; + } + + return ( + + + + + + {placeName} + + {building?.site.name} + + {formatPlaceCategory(building?.category.name)} + + + +
+ + + { + const scheme = Platform.select({ + ios: 'maps://0,0?q=', + android: 'geo:0,0?q=', + }); + const latLng = [ + building?.latitude, + building?.longitude, + ].join(','); + const label = building?.name; + const url = Platform.select({ + ios: `${scheme}${label}@${latLng}`, + android: `${scheme}${latLng}(${label})`, + })!; + Linking.openURL(url); + }} + /> + } + /> + +
+
+
+
+ ); +}; + +const createStyles = ({ fontSizes }: Theme) => + StyleSheet.create({ + title: { + fontSize: fontSizes['2xl'], + }, + }); diff --git a/src/features/places/screens/FreeRoomsScreen.tsx b/src/features/places/screens/FreeRoomsScreen.tsx new file mode 100644 index 00000000..9301ddb4 --- /dev/null +++ b/src/features/places/screens/FreeRoomsScreen.tsx @@ -0,0 +1,182 @@ +import { + LegacyRef, + useEffect, + useLayoutEffect, + useMemo, + useRef, + useState, +} from 'react'; +import { useTranslation } from 'react-i18next'; +import { View } from 'react-native'; +import PagerView from 'react-native-pager-view'; + +import { + faChevronLeft, + faChevronRight, + faMapPin, +} from '@fortawesome/free-solid-svg-icons'; +import { BottomSheetFlatList } from '@gorhom/bottom-sheet'; +import { ActivityIndicator } from '@lib/ui/components/ActivityIndicator'; +import { BottomSheet } from '@lib/ui/components/BottomSheet'; +import { Icon } from '@lib/ui/components/Icon'; +import { IconButton } from '@lib/ui/components/IconButton'; +import { IndentedDivider } from '@lib/ui/components/IndentedDivider'; +import { ListItem, ListItemProps } from '@lib/ui/components/ListItem'; +import { Row } from '@lib/ui/components/Row'; +import { Text } from '@lib/ui/components/Text'; +import { useTheme } from '@lib/ui/hooks/useTheme'; + +import { DateTime } from 'luxon'; + +import { useGetFreeRooms } from '../../../core/queries/placesHooks'; +import { GlobalStyles } from '../../../core/styles/GlobalStyles'; +import { CampusSelector } from '../components/CampusSelector'; +import { IndoorMapLayer } from '../components/IndoorMapLayer'; +import { MapScreenProps } from '../components/MapNavigator'; +import { MarkersLayer } from '../components/MarkersLayer'; +import { PlacesStackParamList } from '../components/PlacesNavigator'; +import { useGetCurrentCampus } from '../hooks/useGetCurrentCampus'; +import { useSearchPlaces } from '../hooks/useSearchPlaces'; +import { PlaceOverviewWithMetadata, SearchPlace } from '../types'; + +type Props = MapScreenProps; + +export const FreeRoomsScreen = ({ navigation }: Props) => { + const { t } = useTranslation(); + const { colors } = useTheme(); + const { spacing, fontSizes } = useTheme(); + const campus = useGetCurrentCampus(); + const [initialDateTime] = useState( + DateTime.now().set({ minute: 0, second: 0, millisecond: 0 }), + ); + const [selectedTimeSlotIndex, setSelectedTimeSlotIndex] = useState(0); + const startDateTime = useMemo( + () => initialDateTime.plus({ hour: 3 * selectedTimeSlotIndex }), + [initialDateTime, selectedTimeSlotIndex], + ); + const { places: sitePlaces } = useSearchPlaces({ siteId: campus?.id }); + const { data: freeRooms, isLoading: isLoadingRooms } = useGetFreeRooms({ + siteId: campus?.id, + date: startDateTime.toISO().split('T')[0], + timeFrom: startDateTime.toISOTime(), + timeTo: startDateTime.plus({ hour: 3 }).toISOTime(), + }); + const places = useMemo( + () => + freeRooms?.data.map(fr => { + const place = sitePlaces?.find(p => p.id === fr.id); + if (!place) return null; + return { ...fr, ...place }; + }) as (SearchPlace & { + freeFrom: Date; + freeTo: Date; + })[], + [freeRooms?.data, sitePlaces], + ); + const pagerRef = useRef(); + const displayFloorId = useMemo(() => { + const floorIds = new Set(freeRooms?.data?.map(r => r.floorId)); + return floorIds.size === 1 ? Array.from(floorIds.values())[0] : undefined; + }, [freeRooms?.data]); + + useEffect(() => { + pagerRef.current?.setPage(selectedTimeSlotIndex); + }, [selectedTimeSlotIndex]); + + useLayoutEffect(() => { + navigation.setOptions({ + headerRight: () => , + mapContent: ( + <> + + + + ), + }); + }, [displayFloorId, navigation, places]); + + return ( + + + + + setSelectedTimeSlotIndex(prevIdx => Math.max(0, prevIdx - 1)) + } + /> + } + style={{ + flexGrow: 1, + height: '100%', + }} + orientation="horizontal" + initialPage={selectedTimeSlotIndex} + onPageSelected={({ nativeEvent: { position } }) => + setSelectedTimeSlotIndex(position) + } + > + {Array.from({ length: selectedTimeSlotIndex + 2 }).map( + (_, slotIndex) => { + const startDate = initialDateTime.plus({ hour: 3 * slotIndex }); + const endDate = startDate.plus({ hour: 3 }); + return ( + + {startDate.toRelativeCalendar()}{' '} + {startDate.toFormat('HH:mm')} - {endDate.toFormat('HH:mm')} + + ); + }, + )} + + setSelectedTimeSlotIndex(prevIdx => prevIdx + 1)} + /> + + ({ + title: + (p as PlaceOverviewWithMetadata).room.name ?? + t('common.untitled'), + subtitle: `${t( + 'common.free', + )} ${p.freeFrom.toLocaleTimeString()} - ${p.freeTo.toLocaleTimeString()}`, + linkTo: { screen: 'Place', params: { placeId: p.id } }, + })) ?? [] + } + renderItem={({ item }: { item: ListItemProps }) => ( + } + {...item} + title={item.title ?? t('common.untitled')} + /> + )} + ItemSeparatorComponent={IndentedDivider} + ListEmptyComponent={ + isLoadingRooms ? ( + + ) : undefined + } + /> + + + ); +}; diff --git a/src/features/places/screens/PlaceScreen.tsx b/src/features/places/screens/PlaceScreen.tsx new file mode 100644 index 00000000..8ce6647d --- /dev/null +++ b/src/features/places/screens/PlaceScreen.tsx @@ -0,0 +1,308 @@ +import { useLayoutEffect } from 'react'; +import { useTranslation } from 'react-i18next'; +import { Dimensions, Linking, Platform, StyleSheet, View } from 'react-native'; +import { useSafeAreaInsets } from 'react-native-safe-area-context'; + +import { + faDiamondTurnRight, + faSignsPost, +} from '@fortawesome/free-solid-svg-icons'; +import { BottomSheetScrollView } from '@gorhom/bottom-sheet'; +import { ActivityIndicator } from '@lib/ui/components/ActivityIndicator'; +import { BottomSheet } from '@lib/ui/components/BottomSheet'; +import { Col } from '@lib/ui/components/Col'; +import { EmptyState } from '@lib/ui/components/EmptyState'; +import { IconButton } from '@lib/ui/components/IconButton'; +import { ListItem } from '@lib/ui/components/ListItem'; +import { OverviewList } from '@lib/ui/components/OverviewList'; +import { Section } from '@lib/ui/components/Section'; +import { SectionHeader } from '@lib/ui/components/SectionHeader'; +import { Text } from '@lib/ui/components/Text'; +import { useStylesheet } from '@lib/ui/hooks/useStylesheet'; +import { useTheme } from '@lib/ui/hooks/useTheme'; +import { Theme } from '@lib/ui/types/Theme'; +import { ResponseError } from '@polito/api-client/runtime'; +import { useHeaderHeight } from '@react-navigation/elements'; +import { FillLayer, LineLayer, ShapeSource } from '@rnmapbox/maps'; + +import { IS_IOS, MAX_RECENT_SEARCHES } from '../../../core/constants'; +import { usePreferencesContext } from '../../../core/contexts/PreferencesContext'; +import { useScreenTitle } from '../../../core/hooks/useScreenTitle'; +import { useGetPlace } from '../../../core/queries/placesHooks'; +import { GlobalStyles } from '../../../core/styles/GlobalStyles'; +import { IndoorMapLayer } from '../components/IndoorMapLayer'; +import { MapScreenProps } from '../components/MapNavigator'; +import { MarkersLayer } from '../components/MarkersLayer'; +import { PlacesStackParamList } from '../components/PlacesNavigator'; +import { useSearchPlaces } from '../hooks/useSearchPlaces'; +import { formatPlaceCategory } from '../utils/category'; + +type Props = MapScreenProps; + +export const PlaceScreen = ({ navigation, route }: Props) => { + const { palettes } = useTheme(); + const styles = useStylesheet(createStyles); + const { t } = useTranslation(); + const { placesSearched, updatePreference } = usePreferencesContext(); + const { fontSizes, spacing } = useTheme(); + const headerHeight = useHeaderHeight(); + const safeAreaInsets = useSafeAreaInsets(); + const { placeId } = route.params; + const { + data: place, + isLoading: isLoadingPlace, + error: getPlaceError, + } = useGetPlace(placeId); + const siteId = place?.data.site.id; + const floorId = place?.data.floor.id; + const { places, isLoading: isLoadingPlaces } = useSearchPlaces({ + siteId, + floorId, + }); + const isLoading = isLoadingPlace || isLoadingPlaces; + const placeName = + place?.data.room.name ?? + place?.data.category.subCategory.name ?? + t('common.untitled'); + + useScreenTitle( + (getPlaceError as ResponseError)?.response?.status === 404 + ? t('common.notFound') + : placeName, + ); + + useLayoutEffect( + () => { + if (place?.data) { + updatePreference('placesSearched', [ + place.data, + ...placesSearched + .filter(p => p.id !== place.data.id) + .slice(0, MAX_RECENT_SEARCHES - 1), + ]); + const { latitude, longitude } = place.data; + navigation.setOptions({ + mapOptions: { + compassPosition: IS_IOS + ? { + top: headerHeight - safeAreaInsets.top + spacing[2], + right: spacing[3], + } + : undefined, + camera: { + centerCoordinate: [longitude, latitude], + padding: { + paddingTop: 0, + paddingLeft: 0, + paddingRight: 0, + paddingBottom: + Dimensions.get('window').height / 2 - headerHeight, + }, + zoomLevel: 19, + }, + }, + mapContent: ( + <> + + + {place.data.geoJson != null && ( + + + + + )} + + ), + }); + } + }, + // eslint-disable-next-line + [], + ); + + if (isLoading) { + return ( + + + + + + ); + } + + if ( + getPlaceError && + (getPlaceError as ResponseError).response.status === 404 + ) { + return ( + + + + + + ); + } + + if (!place) { + return null; + } + + return ( + + + + + + {placeName} + + {place.data.site.name} + + {formatPlaceCategory(place.data.category.name)} + + + + {(place.data.category.id === 'AULA' || + place.data.category.id === 'LAB') && ( +
+ + + + Coming soon + + +
+ )} + +
+ + + { + const scheme = Platform.select({ + ios: 'maps://0,0?q=', + android: 'geo:0,0?q=', + }); + const latLng = [ + place?.data.latitude, + place?.data.longitude, + ].join(','); + const label = place?.data.room.name; + const url = Platform.select({ + ios: `${scheme}${label}@${latLng}`, + android: `${scheme}${latLng}(${label})`, + })!; + Linking.openURL(url); + }} + /> + } + /> + + + {place.data.structure && ( + + )} + {/* */} + +
+ + {(place.data.capacity > 0 || place.data.resources?.length > 0) && ( +
+ + + {place.data.capacity > 0 && ( + + )} + {place.data.resources?.map(r => ( + + ))} + +
+ )} +
+
+
+ ); +}; + +const createStyles = ({ fontSizes }: Theme) => + StyleSheet.create({ + title: { + fontSize: fontSizes['2xl'], + }, + }); diff --git a/src/features/places/screens/PlacesScreen.tsx b/src/features/places/screens/PlacesScreen.tsx index 31a68e7a..e4e76f7c 100644 --- a/src/features/places/screens/PlacesScreen.tsx +++ b/src/features/places/screens/PlacesScreen.tsx @@ -1,21 +1,495 @@ +import { + useCallback, + useContext, + useEffect, + useLayoutEffect, + useMemo, + useState, +} from 'react'; import { useTranslation } from 'react-i18next'; -import { ScrollView } from 'react-native'; +import { Dimensions, StyleSheet, TouchableOpacity, View } from 'react-native'; +import Animated, { + Extrapolate, + interpolate, + useAnimatedStyle, + useSharedValue, +} from 'react-native-reanimated'; +import { useSafeAreaInsets } from 'react-native-safe-area-context'; -import { faTriangleExclamation } from '@fortawesome/free-solid-svg-icons'; +import { faClock } from '@fortawesome/free-regular-svg-icons'; +import { + faBookOpen, + faBookReader, + faChalkboardTeacher, + faChevronDown, + faCrosshairs, + faElevator, + faEllipsis, + faExpand, +} from '@fortawesome/free-solid-svg-icons'; +import { Divider } from '@lib/ui/components/Divider'; import { EmptyState } from '@lib/ui/components/EmptyState'; +import { Icon } from '@lib/ui/components/Icon'; +import { IconButton } from '@lib/ui/components/IconButton'; +import { PillButton } from '@lib/ui/components/PillButton'; +import { PillIconButton } from '@lib/ui/components/PillIconButton'; +import { Row } from '@lib/ui/components/Row'; +import { Tabs } from '@lib/ui/components/Tabs'; +import { Text } from '@lib/ui/components/Text'; +import { TranslucentCard } from '@lib/ui/components/TranslucentCard'; +import { ThemeContext } from '@lib/ui/contexts/ThemeContext'; +import { useStylesheet } from '@lib/ui/hooks/useStylesheet'; import { useTheme } from '@lib/ui/hooks/useTheme'; +import { Theme } from '@lib/ui/types/Theme'; +import { MenuView } from '@react-native-menu/menu'; +import { useHeaderHeight } from '@react-navigation/elements'; +import Mapbox, { CameraPadding } from '@rnmapbox/maps'; -export const PlacesScreen = () => { - const { palettes } = useTheme(); +import { debounce } from 'lodash'; + +import { HeaderLogo } from '../../../core/components/HeaderLogo'; +import { IS_IOS } from '../../../core/constants'; +import { usePreferencesContext } from '../../../core/contexts/PreferencesContext'; +import { useScreenTitle } from '../../../core/hooks/useScreenTitle'; +import { + useGetPlaceCategory, + useGetPlaceSubCategory, + useGetSites, +} from '../../../core/queries/placesHooks'; +import { GlobalStyles } from '../../../core/styles/GlobalStyles'; +import { darkTheme } from '../../../core/themes/dark'; +import { CampusSelector } from '../components/CampusSelector'; +import { IndoorMapLayer } from '../components/IndoorMapLayer'; +import { MapScreenProps } from '../components/MapNavigator'; +import { MarkersLayer } from '../components/MarkersLayer'; +import { PlaceCategoriesBottomSheet } from '../components/PlaceCategoriesBottomSheet'; +import { PlacesBottomSheet } from '../components/PlacesBottomSheet'; +import { PlacesStackParamList } from '../components/PlacesNavigator'; +import { MapNavigatorContext } from '../contexts/MapNavigatorContext'; +import { useGetCurrentCampus } from '../hooks/useGetCurrentCampus'; +import { useSearchPlaces } from '../hooks/useSearchPlaces'; +import { PlaceOverviewWithMetadata, isPlace } from '../types'; +import { formatPlaceCategory } from '../utils/category'; +import { formatAgendaItem } from '../utils/formatAgendaItem'; + +type Props = MapScreenProps; + +export const PlacesScreen = ({ navigation, route }: Props) => { + const { categoryId, subCategoryId, bounds } = route.params ?? {}; + const placeCategory = useGetPlaceCategory(categoryId); + const placeSubCategory = useGetPlaceSubCategory(subCategoryId); const { t } = useTranslation(); + const styles = useStylesheet(createStyles); + const { spacing, fontSizes } = useTheme(); + const [categoriesPanelOpen, setCategoriesPanelOpen] = useState(false); + const headerHeight = useHeaderHeight(); + const [tabsHeight, setTabsHeight] = useState(46); + const { data: sites } = useGetSites(); + const campus = useGetCurrentCampus(); + const { updatePreference, placesSearched } = usePreferencesContext(); + const safeAreaInsets = useSafeAreaInsets(); + const { cameraRef } = useContext(MapNavigatorContext); + const [search, setSearch] = useState(''); + const [floorId, setFloorId] = useState(); + const [debouncedSearch, setDebouncedSearch] = useState(search); + const bottomSheetPosition = useSharedValue(0); + const [screenHeight, setScreenHeight] = useState( + Dimensions.get('window').height, + ); + + // eslint-disable-next-line react-hooks/exhaustive-deps + const updateDebouncedSearch = useCallback( + debounce( + (newSearch: string) => setDebouncedSearch(newSearch.trim().toLowerCase()), + 200, + ), + [], + ); + + useEffect(() => { + updateDebouncedSearch(search); + }, [search, updateDebouncedSearch]); + + const { places, isLoading: isLoadingPlaces } = useSearchPlaces({ + search: debouncedSearch, + floorId, + categoryId, + subCategoryId, + }); + + const categoryFilterName = useMemo( + () => formatPlaceCategory(placeSubCategory?.name ?? placeCategory?.name), + [placeCategory, placeSubCategory], + ); + + useScreenTitle(categoryFilterName); + + const centerToUserLocation = useCallback(async () => { + const location = await Mapbox.locationManager.getLastKnownLocation(); + if (location) { + const { latitude, longitude } = location.coords; + cameraRef.current?.flyTo([longitude, latitude]); + } + }, [cameraRef]); + + const centerToCurrentCampus = useCallback(async () => { + if (!campus || !cameraRef.current) { + return; + } + const { latitude, longitude } = campus; + cameraRef.current.fitBounds( + [longitude - campus.extent, latitude - campus.extent], + [longitude + campus.extent, latitude + campus.extent], + undefined, + 2000, + ); + }, [cameraRef, campus]); + + useEffect(() => { + if (!campus) { + if (sites?.data?.length) { + updatePreference('campusId', sites?.data[0].id); + } + } else { + if (!floorId && campus.floors?.length) { + setFloorId( + campus.floors.find(f => f.id === 'XPTE')?.id ?? + campus.floors.find(f => f.level === 0)?.id, + ); + } + centerToCurrentCampus(); + } + }, [campus, sites, centerToCurrentCampus, floorId, updatePreference]); + + const displayFloorId = useMemo(() => { + if (debouncedSearch) { + const floorIds = new Set( + places + ?.filter(p => isPlace(p)) + .map(p => (p as PlaceOverviewWithMetadata).floor.id), + ); + return floorIds.size === 1 ? [...floorIds.values()][0] : undefined; + } + return floorId; + }, [debouncedSearch, floorId, places]); + + const categoryFilterActive = categoryId || subCategoryId; + + useLayoutEffect(() => { + const mapInsetTop = + headerHeight - + safeAreaInsets.top + + (!categoryFilterActive ? tabsHeight : 0); + navigation.setOptions({ + headerLeft: !categoryFilterActive + ? () => + : undefined, + headerRight: () => , + mapOptions: { + compassPosition: IS_IOS + ? { + top: mapInsetTop + spacing[2], + right: spacing[3], + } + : undefined, + camera: { + padding: { + paddingTop: mapInsetTop, + } as CameraPadding, + bounds: + bounds ?? + (campus + ? { + ne: [ + campus.longitude - campus.extent, + campus.latitude - campus.extent, + ], + sw: [ + campus.longitude + campus.extent, + campus.latitude + campus.extent, + ], + } + : undefined), + }, + }, + mapContent: ( + <> + + + + ), + }); + }, [ + campus, + displayFloorId, + floorId, + headerHeight, + navigation, + categoryId, + route, + safeAreaInsets.top, + debouncedSearch, + spacing, + tabsHeight, + categoryFilterActive, + bounds, + places, + ]); + + const controlsAnimatedStyle = useAnimatedStyle(() => { + const opacity = interpolate( + bottomSheetPosition.value, + [0.65 * screenHeight, 0.7 * screenHeight], + [0, 1], + Extrapolate.CLAMP, + ); + + return { + opacity, + transform: [ + { + translateY: Math.max(0.7 * screenHeight, bottomSheetPosition.value), + }, + ], + }; + }); + + const listPlaces = useMemo(() => { + if (!search && !categoryId && !subCategoryId) { + return places?.filter(p => isPlace(p) && p.room.name != null); + } + return places; + }, [categoryId, places, search, subCategoryId]); + + const floorSelectorButton = ( + + + + + {campus?.floors.find(f => f.id === floorId)?.name} + + + + + ); return ( - - setScreenHeight(height)} + > + {!categoryFilterActive && ( + setTabsHeight(height)} + > + navigation.navigate('FreeRooms')} + > + {t('freeRoomsScreen.title')} + + + navigation.navigate({ + name: 'Places', + key: 'Places:AULA', + params: { subCategoryId: 'AULA' }, + }) + } + > + {t('placeCategories.classrooms')} + + + navigation.navigate({ + name: 'Places', + key: 'Places:S_STUD', + params: { subCategoryId: 'S_STUD' }, + }) + } + > + {t('placeCategories.studyRooms')} + + + navigation.navigate({ + name: 'Places', + key: 'Places:BIBLIO', + params: { subCategoryId: 'BIBLIO' }, + }) + } + > + {t('placeCategories.libraries')} + + {/* TODO + navigation.navigate({ + name: 'Places', + key: 'Places:student', + params: { categoryId: 'student' }, + }) + } + > + Student services + */} + setCategoriesPanelOpen(true)}> + + + + More + + + + + )} + + + + + + + + + + {campus?.floors?.length ? ( + !debouncedSearch ? ( + { + setFloorId(selectedFloorId); + }} + actions={campus.floors + .sort((a, b) => a.level - b.level) + .map(f => ({ + id: f.id, + title: f.name, + subtitle: f.level.toString(), + }))} + > + {floorSelectorButton} + + ) : displayFloorId ? ( + floorSelectorButton + ) : ( + + + + + {t('placesScreen.multipleFloors')} + + + + ) + ) : null} + + + + ({ + title: isPlace(p) + ? p.room.name ?? p.category.subCategory.name + : p.name, + subtitle: isPlace(p) + ? `${ + p.agendaItem != null + ? formatAgendaItem(p.agendaItem) + : placesSearched.some(ps => ps.id === p.id) + ? t('common.recentlyViewed') + : p.category.name + } - ${p.floor.name}` + : t('common.building'), + linkTo: isPlace(p) + ? { screen: 'Place', params: { placeId: p.id } } + : { screen: 'Building', params: { buildingId: p.id } }, + })) ?? [], + ListEmptyComponent: ( + + ), + }} /> - + + setCategoriesPanelOpen(false)} + /> +
); }; + +const createStyles = ({ spacing }: Theme) => + StyleSheet.create({ + controls: { + position: 'absolute', + top: 0, + left: spacing[5], + right: spacing[5], + marginTop: -58, + }, + divider: { + alignSelf: 'stretch', + }, + icon: { + paddingHorizontal: spacing[3], + paddingVertical: spacing[2.5], + }, + }); diff --git a/src/features/places/types.ts b/src/features/places/types.ts new file mode 100644 index 00000000..c5fb76ab --- /dev/null +++ b/src/features/places/types.ts @@ -0,0 +1,28 @@ +import { Palette } from '@lib/ui/types/Theme'; +import { Building, PlaceOverview } from '@polito/api-client'; + +import { AgendaItem } from '../agenda/types/AgendaItem'; + +export interface CategoryData { + icon: string; + color: string; + shade?: keyof Palette; + priority: number; + children: Record>; +} + +export type PlaceOverviewWithMetadata = PlaceOverview & { + type: 'place'; + agendaItem?: AgendaItem; +}; + +export type BuildingWithMetadata = Building & { + type: 'building'; +}; + +export type SearchPlace = PlaceOverviewWithMetadata | BuildingWithMetadata; + +export const isPlace = ( + placeOrBuilding: SearchPlace, +): placeOrBuilding is PlaceOverviewWithMetadata => + placeOrBuilding.type === 'place'; diff --git a/src/features/places/utils/category.ts b/src/features/places/utils/category.ts new file mode 100644 index 00000000..815d7a32 --- /dev/null +++ b/src/features/places/utils/category.ts @@ -0,0 +1,2 @@ +export const formatPlaceCategory = (name?: string) => + name?.length ? name.split(' - ')[1] ?? name : undefined; diff --git a/src/features/places/utils/formatAgendaItem.ts b/src/features/places/utils/formatAgendaItem.ts new file mode 100644 index 00000000..1e7775ab --- /dev/null +++ b/src/features/places/utils/formatAgendaItem.ts @@ -0,0 +1,8 @@ +import { AgendaItem } from '../../agenda/types/AgendaItem'; + +export const formatAgendaItem = (agendaItem: AgendaItem, ellipsize = false) => + `${ + !ellipsize || agendaItem.title.length < 10 + ? agendaItem.title + : agendaItem.title.substring(0, 8) + '...' + } @${agendaItem.start.toFormat('hh:mm')}`; diff --git a/src/features/places/utils/resolvePlaceId.ts b/src/features/places/utils/resolvePlaceId.ts new file mode 100644 index 00000000..685c0f70 --- /dev/null +++ b/src/features/places/utils/resolvePlaceId.ts @@ -0,0 +1,4 @@ +import { LecturePlace } from '@polito/api-client/models/LecturePlace'; + +export const resolvePlaceId = (place: LecturePlace) => + [place.buildingId, place.floorId, place.roomId].join('-'); diff --git a/src/features/services/components/MessagingView.tsx b/src/features/services/components/MessagingView.tsx index 1a633c42..2488e19f 100644 --- a/src/features/services/components/MessagingView.tsx +++ b/src/features/services/components/MessagingView.tsx @@ -15,7 +15,7 @@ import { MenuView } from '@react-native-menu/menu'; import { TranslucentView } from '../../../core/components/TranslucentView'; import { IS_IOS } from '../../../core/constants'; -import { GlobalStyles } from '../../../core/styles/globalStyles'; +import { GlobalStyles } from '../../../core/styles/GlobalStyles'; import { pdfSizes } from '../../teaching/constants'; import { Attachment } from '../types/Attachment'; import { AttachmentChip } from './AttachmentChip'; diff --git a/src/features/services/components/PersonOverviewListItem.tsx b/src/features/services/components/PersonOverviewListItem.tsx index b52609e0..258885a0 100644 --- a/src/features/services/components/PersonOverviewListItem.tsx +++ b/src/features/services/components/PersonOverviewListItem.tsx @@ -17,14 +17,13 @@ import { useNavigation } from '@react-navigation/native'; import { NativeStackNavigationProp } from '@react-navigation/native-stack'; import { useQueryClient } from '@tanstack/react-query'; +import { MAX_RECENT_SEARCHES } from '../../../core/constants'; import { usePreferencesContext } from '../../../core/contexts/PreferencesContext'; import { useAccessibility } from '../../../core/hooks/useAccessibilty'; import { useOfflineDisabled } from '../../../core/hooks/useOfflineDisabled'; import { getPersonKey } from '../../../core/queries/peopleHooks'; import { HighlightedText } from './HighlightedText'; -const maxRecentSearches = 10; - interface Props { person: PersonOverview; searchString?: string; @@ -57,7 +56,7 @@ export const PersonOverviewListItem = ({ const navigateToPerson = () => { const personIndex = peopleSearched.findIndex(p => p.id === person.id); if (personIndex === -1) { - if (peopleSearched.length >= maxRecentSearches) { + if (peopleSearched.length >= MAX_RECENT_SEARCHES) { peopleSearched.pop(); } updatePreference('peopleSearched', [person, ...peopleSearched]); diff --git a/src/features/services/components/TicketStatusInfo.tsx b/src/features/services/components/TicketStatusInfo.tsx index 654d85be..1bbbfe05 100644 --- a/src/features/services/components/TicketStatusInfo.tsx +++ b/src/features/services/components/TicketStatusInfo.tsx @@ -14,7 +14,7 @@ import { useTheme } from '@lib/ui/hooks/useTheme'; import { Theme } from '@lib/ui/types/Theme'; import { TicketOverview, TicketStatus } from '@polito/api-client'; -import { GlobalStyles } from '../../../core/styles/globalStyles'; +import { GlobalStyles } from '../../../core/styles/GlobalStyles'; import { formatDate, formatDateTime } from '../../../utils/dates'; interface TicketStatusProps { diff --git a/src/features/services/screens/ContactsScreen.tsx b/src/features/services/screens/ContactsScreen.tsx index a80e6ddd..773d8edf 100644 --- a/src/features/services/screens/ContactsScreen.tsx +++ b/src/features/services/screens/ContactsScreen.tsx @@ -19,7 +19,7 @@ import { usePreferencesContext } from '../../../core/contexts/PreferencesContext import { useDebounceValue } from '../../../core/hooks/useDebounceValue'; import { useOfflineDisabled } from '../../../core/hooks/useOfflineDisabled'; import { useGetPeople } from '../../../core/queries/peopleHooks'; -import { GlobalStyles } from '../../../core/styles/globalStyles'; +import { GlobalStyles } from '../../../core/styles/GlobalStyles'; import { PersonOverviewListItem } from '../components/PersonOverviewListItem'; import { RecentSearch } from '../components/RecentSearch'; diff --git a/src/features/services/screens/CreateTicketScreen.tsx b/src/features/services/screens/CreateTicketScreen.tsx index df0c335f..227a5160 100644 --- a/src/features/services/screens/CreateTicketScreen.tsx +++ b/src/features/services/screens/CreateTicketScreen.tsx @@ -21,7 +21,7 @@ import { useCreateTicket, useGetTicketTopics, } from '../../../core/queries/ticketHooks'; -import { GlobalStyles } from '../../../core/styles/globalStyles'; +import { GlobalStyles } from '../../../core/styles/GlobalStyles'; import { darkTheme } from '../../../core/themes/dark'; import { MessagingView } from '../components/MessagingView'; import { ServiceStackParamList } from '../components/ServicesNavigator'; diff --git a/src/features/services/screens/ServicesScreen.tsx b/src/features/services/screens/ServicesScreen.tsx index 281c1701..113d1437 100644 --- a/src/features/services/screens/ServicesScreen.tsx +++ b/src/features/services/screens/ServicesScreen.tsx @@ -90,7 +90,7 @@ export const ServicesScreen = () => { id: 'contacts', name: t('contactsScreen.title'), icon: faIdCard, - disabled: isOffline && peopleSearched?.length === 0, + disabled: isOffline && peopleSearched?.length === 0, // TODO why? linkTo: { screen: 'Contacts' }, }, { diff --git a/src/features/services/screens/TicketFaqsScreen.tsx b/src/features/services/screens/TicketFaqsScreen.tsx index 1fe806aa..4dabd6b2 100644 --- a/src/features/services/screens/TicketFaqsScreen.tsx +++ b/src/features/services/screens/TicketFaqsScreen.tsx @@ -33,7 +33,7 @@ import { parseDocument } from 'htmlparser2'; import { BottomBarSpacer } from '../../../core/components/BottomBarSpacer'; import { useSearchTicketFaqs } from '../../../core/queries/ticketHooks'; -import { GlobalStyles } from '../../../core/styles/globalStyles'; +import { GlobalStyles } from '../../../core/styles/GlobalStyles'; import { ServiceStackParamList } from '../components/ServicesNavigator'; type Props = NativeStackScreenProps; diff --git a/src/features/services/screens/TicketScreen.tsx b/src/features/services/screens/TicketScreen.tsx index de6b38ff..07a45ac5 100644 --- a/src/features/services/screens/TicketScreen.tsx +++ b/src/features/services/screens/TicketScreen.tsx @@ -28,7 +28,7 @@ import { useMarkTicketAsClosed, useMarkTicketAsRead, } from '../../../core/queries/ticketHooks'; -import { GlobalStyles } from '../../../core/styles/globalStyles'; +import { GlobalStyles } from '../../../core/styles/GlobalStyles'; import { ChatMessage } from '../components/ChatMessage'; import { ServiceStackParamList } from '../components/ServicesNavigator'; import { TextMessage } from '../components/TextMessage'; diff --git a/src/features/teaching/screens/CourseAssignmentUploadConfirmationScreen.tsx b/src/features/teaching/screens/CourseAssignmentUploadConfirmationScreen.tsx index 972e3cc4..4fd6dbae 100644 --- a/src/features/teaching/screens/CourseAssignmentUploadConfirmationScreen.tsx +++ b/src/features/teaching/screens/CourseAssignmentUploadConfirmationScreen.tsx @@ -22,7 +22,7 @@ import { NativeStackScreenProps } from '@react-navigation/native-stack'; import { useFeedbackContext } from '../../../core/contexts/FeedbackContext'; import { useUploadAssignment } from '../../../core/queries/courseHooks'; -import { GlobalStyles } from '../../../core/styles/globalStyles'; +import { GlobalStyles } from '../../../core/styles/GlobalStyles'; import { TeachingStackParamList } from '../components/TeachingNavigator'; type Props = NativeStackScreenProps< diff --git a/src/features/teaching/screens/CourseIconPickerScreen.tsx b/src/features/teaching/screens/CourseIconPickerScreen.tsx index 4d7171aa..90253cee 100644 --- a/src/features/teaching/screens/CourseIconPickerScreen.tsx +++ b/src/features/teaching/screens/CourseIconPickerScreen.tsx @@ -8,7 +8,10 @@ import { useTheme } from '@lib/ui/hooks/useTheme'; import { NativeStackScreenProps } from '@react-navigation/native-stack'; import { BottomBarSpacer } from '../../../core/components/BottomBarSpacer'; -import { usePreferencesContext } from '../../../core/contexts/PreferencesContext'; +import { + PreferencesContextBase, + usePreferencesContext, +} from '../../../core/contexts/PreferencesContext'; import { useSafeAreaSpacing } from '../../../core/hooks/useSafeAreaSpacing'; import { TeachingStackParamList } from '../components/TeachingNavigator'; import { courseIcons } from '../constants'; @@ -93,7 +96,7 @@ export const CourseIconPickerScreen = ({ navigation, route }: Props) => { ...coursePrefs, icon: null, }, - }); + } as PreferencesContextBase['courses']); navigation.goBack(); }} /> diff --git a/src/features/teaching/screens/CourseInfoScreen.tsx b/src/features/teaching/screens/CourseInfoScreen.tsx index b2413a98..882d7b31 100644 --- a/src/features/teaching/screens/CourseInfoScreen.tsx +++ b/src/features/teaching/screens/CourseInfoScreen.tsx @@ -27,7 +27,7 @@ import { useGetCourseExams, } from '../../../core/queries/courseHooks'; import { useGetPersons } from '../../../core/queries/peopleHooks'; -import { GlobalStyles } from '../../../core/styles/globalStyles'; +import { GlobalStyles } from '../../../core/styles/GlobalStyles'; import { ExamListItem } from '../components/ExamListItem'; import { useCourseContext } from '../contexts/CourseContext'; diff --git a/src/features/teaching/screens/CourseNoticesScreen.tsx b/src/features/teaching/screens/CourseNoticesScreen.tsx index 94ff4822..c3ead452 100644 --- a/src/features/teaching/screens/CourseNoticesScreen.tsx +++ b/src/features/teaching/screens/CourseNoticesScreen.tsx @@ -16,7 +16,7 @@ import { useAccessibility } from '../../../core/hooks/useAccessibilty'; import { useOfflineDisabled } from '../../../core/hooks/useOfflineDisabled'; import { useSafeAreaSpacing } from '../../../core/hooks/useSafeAreaSpacing'; import { useGetCourseNotices } from '../../../core/queries/courseHooks'; -import { GlobalStyles } from '../../../core/styles/globalStyles'; +import { GlobalStyles } from '../../../core/styles/GlobalStyles'; import { formatDate } from '../../../utils/dates'; import { getHtmlTextContent } from '../../../utils/html'; import { useCourseContext } from '../contexts/CourseContext'; diff --git a/src/features/teaching/screens/CourseVideolectureScreen.tsx b/src/features/teaching/screens/CourseVideolectureScreen.tsx index a7b085a0..39fb771f 100644 --- a/src/features/teaching/screens/CourseVideolectureScreen.tsx +++ b/src/features/teaching/screens/CourseVideolectureScreen.tsx @@ -12,7 +12,7 @@ import { EventDetails } from '../../../core/components/EventDetails'; import { VideoPlayer } from '../../../core/components/VideoPlayer'; import { useGetCourseVideolectures } from '../../../core/queries/courseHooks'; import { useGetPerson } from '../../../core/queries/peopleHooks'; -import { GlobalStyles } from '../../../core/styles/globalStyles'; +import { GlobalStyles } from '../../../core/styles/GlobalStyles'; import { formatDateWithTimeIfNotNull } from '../../../utils/dates'; import { TeachingStackParamList } from '../components/TeachingNavigator'; diff --git a/src/features/teaching/screens/CourseVirtualClassroomScreen.tsx b/src/features/teaching/screens/CourseVirtualClassroomScreen.tsx index da7e9a77..aa68cac6 100644 --- a/src/features/teaching/screens/CourseVirtualClassroomScreen.tsx +++ b/src/features/teaching/screens/CourseVirtualClassroomScreen.tsx @@ -12,7 +12,7 @@ import { EventDetails } from '../../../core/components/EventDetails'; import { VideoPlayer } from '../../../core/components/VideoPlayer'; import { useGetCourseVirtualClassrooms } from '../../../core/queries/courseHooks'; import { useGetPerson } from '../../../core/queries/peopleHooks'; -import { GlobalStyles } from '../../../core/styles/globalStyles'; +import { GlobalStyles } from '../../../core/styles/GlobalStyles'; import { formatDateWithTimeIfNotNull } from '../../../utils/dates'; import { TeachingStackParamList } from '../components/TeachingNavigator'; import { isLiveVC, isRecordedVC } from '../utils/lectures'; diff --git a/src/features/teaching/screens/CoursesScreen.tsx b/src/features/teaching/screens/CoursesScreen.tsx index 33306890..ff110329 100644 --- a/src/features/teaching/screens/CoursesScreen.tsx +++ b/src/features/teaching/screens/CoursesScreen.tsx @@ -37,7 +37,10 @@ export const CoursesScreen = () => { (byPeriod[course.teachingPeriod] = byPeriod[course.teachingPeriod] ?? []).push(course); return byPeriod; - }, {} as Record), + }, {} as Record) as Record< + string, + CourseOverview[] + >, ).map(([period, courses]) => (
; diff --git a/src/features/teaching/screens/ExamScreen.tsx b/src/features/teaching/screens/ExamScreen.tsx index 7ef66621..d431ab38 100644 --- a/src/features/teaching/screens/ExamScreen.tsx +++ b/src/features/teaching/screens/ExamScreen.tsx @@ -43,7 +43,6 @@ export const ExamScreen = ({ route, navigation }: Props) => { const { colors, fontSizes, spacing } = useTheme(); const examsQuery = useGetExams(); const exam = examsQuery.data?.find(e => e.id === id); - const teacherQuery = useGetPerson(exam?.teacherId); const routes = navigation.getState()?.routes; diff --git a/src/features/teaching/screens/TeachingScreen.tsx b/src/features/teaching/screens/TeachingScreen.tsx index ee790cab..506cade0 100644 --- a/src/features/teaching/screens/TeachingScreen.tsx +++ b/src/features/teaching/screens/TeachingScreen.tsx @@ -31,7 +31,7 @@ import { useOfflineDisabled } from '../../../core/hooks/useOfflineDisabled'; import { useGetCourses } from '../../../core/queries/courseHooks'; import { useGetExams } from '../../../core/queries/examHooks'; import { useGetStudent } from '../../../core/queries/studentHooks'; -import { GlobalStyles } from '../../../core/styles/globalStyles'; +import { GlobalStyles } from '../../../core/styles/GlobalStyles'; import { formatFinalGrade } from '../../../utils/grades'; import { CourseListItem } from '../components/CourseListItem'; import { ExamListItem } from '../components/ExamListItem'; diff --git a/src/features/teaching/screens/TranscriptScreen.tsx b/src/features/teaching/screens/TranscriptScreen.tsx index a956d40c..f7e0deaf 100644 --- a/src/features/teaching/screens/TranscriptScreen.tsx +++ b/src/features/teaching/screens/TranscriptScreen.tsx @@ -22,7 +22,7 @@ import { useGetGrades, useGetStudent, } from '../../../core/queries/studentHooks'; -import { GlobalStyles } from '../../../core/styles/globalStyles'; +import { GlobalStyles } from '../../../core/styles/GlobalStyles'; import { formatDate } from '../../../utils/dates'; import { formatFinalGrade, formatGrade } from '../../../utils/grades'; import { ProgressChart } from '../components/ProgressChart'; diff --git a/src/features/user/screens/SettingsScreen.tsx b/src/features/user/screens/SettingsScreen.tsx index c8a36ca3..56449d06 100644 --- a/src/features/user/screens/SettingsScreen.tsx +++ b/src/features/user/screens/SettingsScreen.tsx @@ -34,7 +34,10 @@ import { Settings } from 'luxon'; import { BottomBarSpacer } from '../../../core/components/BottomBarSpacer'; import { useFeedbackContext } from '../../../core/contexts/FeedbackContext'; -import { usePreferencesContext } from '../../../core/contexts/PreferencesContext'; +import { + PreferencesContextBase, + usePreferencesContext, +} from '../../../core/contexts/PreferencesContext'; import { useConfirmationDialog } from '../../../core/hooks/useConfirmationDialog'; import { useOfflineDisabled } from '../../../core/hooks/useOfflineDisabled'; import { useUpdateDevicePreferences } from '../../../core/queries/studentHooks'; @@ -177,7 +180,10 @@ const VisualizationListItem = () => { }; })} onPressAction={({ nativeEvent: { event } }) => { - updatePreference('colorScheme', event); + updatePreference( + 'colorScheme', + event as PreferencesContextBase['colorScheme'], + ); }} > { updatePreference('notifications', { ...notifications, [notificationType]: value, - }); + } as PreferencesContextBase['notifications']); }; return (