├── .node-version ├── .ruby-version ├── android ├── app │ ├── src │ │ ├── main │ │ │ ├── res │ │ │ │ ├── values-night │ │ │ │ │ └── colors.xml │ │ │ │ ├── mipmap-hdpi │ │ │ │ │ ├── ic_launcher.png │ │ │ │ │ ├── ic_launcher_round.png │ │ │ │ │ └── ic_launcher_foreground.png │ │ │ │ ├── mipmap-mdpi │ │ │ │ │ ├── ic_launcher.png │ │ │ │ │ ├── ic_launcher_round.png │ │ │ │ │ └── ic_launcher_foreground.png │ │ │ │ ├── mipmap-xhdpi │ │ │ │ │ ├── ic_launcher.png │ │ │ │ │ ├── ic_launcher_round.png │ │ │ │ │ └── ic_launcher_foreground.png │ │ │ │ ├── mipmap-xxhdpi │ │ │ │ │ ├── ic_launcher.png │ │ │ │ │ ├── ic_launcher_round.png │ │ │ │ │ └── ic_launcher_foreground.png │ │ │ │ ├── mipmap-xxxhdpi │ │ │ │ │ ├── ic_launcher.png │ │ │ │ │ ├── ic_launcher_round.png │ │ │ │ │ └── ic_launcher_foreground.png │ │ │ │ ├── drawable-hdpi │ │ │ │ │ └── splashscreen_image.png │ │ │ │ ├── drawable-mdpi │ │ │ │ │ └── splashscreen_image.png │ │ │ │ ├── drawable-xhdpi │ │ │ │ │ └── splashscreen_image.png │ │ │ │ ├── drawable-xxhdpi │ │ │ │ │ └── splashscreen_image.png │ │ │ │ ├── drawable-xxxhdpi │ │ │ │ │ └── splashscreen_image.png │ │ │ │ ├── drawable │ │ │ │ │ ├── splashscreen.xml │ │ │ │ │ └── rn_edit_text_material.xml │ │ │ │ ├── values │ │ │ │ │ ├── colors.xml │ │ │ │ │ ├── strings.xml │ │ │ │ │ └── styles.xml │ │ │ │ └── mipmap-anydpi-v26 │ │ │ │ │ ├── ic_launcher.xml │ │ │ │ │ └── ic_launcher_round.xml │ │ │ ├── java │ │ │ │ └── com │ │ │ │ │ └── tamotam │ │ │ │ │ └── application │ │ │ │ │ ├── MainApplication.java │ │ │ │ │ └── MainActivity.java │ │ │ └── AndroidManifest.xml │ │ ├── debug │ │ │ ├── AndroidManifest.xml │ │ │ └── java │ │ │ │ └── com │ │ │ │ └── tamotam │ │ │ │ └── application │ │ │ │ └── ReactNativeFlipper.java │ │ └── release │ │ │ └── java │ │ │ └── com │ │ │ └── tamotam │ │ │ └── application │ │ │ └── ReactNativeFlipper.java │ ├── debug.keystore │ ├── proguard-rules.pro │ └── build.gradle ├── gradle │ └── wrapper │ │ ├── gradle-wrapper.jar │ │ └── gradle-wrapper.properties ├── .gitignore ├── settings.gradle ├── gradle.properties ├── build.gradle ├── gradlew.bat └── gradlew ├── ios ├── Podfile.properties.json ├── TamoTam │ ├── Images.xcassets │ │ ├── Contents.json │ │ ├── SplashScreen.imageset │ │ │ ├── image.png │ │ │ └── Contents.json │ │ ├── AppIcon.appiconset │ │ │ ├── ItunesArtwork@2x.png │ │ │ ├── App-Icon-20x20@1x.png │ │ │ ├── App-Icon-20x20@2x.png │ │ │ ├── App-Icon-20x20@3x.png │ │ │ ├── App-Icon-29x29@1x.png │ │ │ ├── App-Icon-29x29@2x.png │ │ │ ├── App-Icon-29x29@3x.png │ │ │ ├── App-Icon-40x40@1x.png │ │ │ ├── App-Icon-40x40@2x.png │ │ │ ├── App-Icon-40x40@3x.png │ │ │ ├── App-Icon-60x60@2x.png │ │ │ ├── App-Icon-60x60@3x.png │ │ │ ├── App-Icon-76x76@1x.png │ │ │ ├── App-Icon-76x76@2x.png │ │ │ ├── App-Icon-83.5x83.5@2x.png │ │ │ └── Contents.json │ │ └── SplashScreenBackground.imageset │ │ │ ├── image.png │ │ │ └── Contents.json │ ├── noop-file.swift │ ├── AppDelegate.h │ ├── main.m │ ├── TamoTam.entitlements │ ├── Supporting │ │ └── Expo.plist │ ├── Info.plist │ ├── AppDelegate.mm │ └── SplashScreen.storyboard ├── TamoTam.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ └── IDEWorkspaceChecks.plist ├── .gitignore ├── .xcode.env ├── TamoTam.xcodeproj │ └── xcshareddata │ │ └── xcschemes │ │ └── TamoTam.xcscheme └── Podfile ├── .bundle └── config ├── docs ├── star.webm ├── AppleAppStoreButton.png ├── GooglePlayStoreButton.png └── dataFlowArchitecture.png ├── assets ├── images │ ├── icon.png │ ├── favicon.png │ ├── no-image.jpeg │ ├── icon-map-user-event.png │ └── icon-map-tamotam-event.png └── fonts │ ├── Boiling-BlackDemo.ttf │ └── SpaceMono-Regular.ttf ├── interfaces ├── coordinate.tsx ├── region.tsx └── event.tsx ├── tsconfig.json ├── .prettierrc.js ├── metro.config.js ├── babel.config.js ├── Gemfile ├── constants ├── Layout.ts ├── Colors.ts └── CustomMapStyles.ts ├── components ├── StyledText.tsx ├── TabBarIcon.tsx ├── __tests__ │ └── StyledText-test.js ├── MaterialHeaderButton.tsx ├── Themed.tsx ├── EventItem.tsx └── SelectImage.tsx ├── .expo-shared └── assets.json ├── index.js ├── navigation ├── LinkingConfiguration.ts ├── index.tsx └── BottomTabNavigator.tsx ├── firebase.json ├── types.tsx ├── hooks ├── useColorScheme.ts └── useCachedResources.ts ├── eas.json ├── app.json ├── common ├── readItemFromStorage.ts └── writeItemToStorage.ts ├── screens ├── NotFoundScreen.tsx ├── EventDetailScreen.tsx └── SavedScreen.tsx ├── package.json ├── store ├── actions │ ├── apis │ │ ├── predictHqEvents.tsx │ │ ├── seatGeekEvents.tsx │ │ ├── triRegEvents.tsx │ │ ├── runRegEvents.tsx │ │ ├── bikeRegEvents.tsx │ │ ├── skiRegEvents.tsx │ │ ├── usersEvents.tsx │ │ └── ticketmasterEvents.tsx │ └── events.tsx └── reducers │ └── events.tsx ├── App.tsx ├── .gitignore ├── README.md └── helpers └── sqlite_db.ts /.node-version: -------------------------------------------------------------------------------- 1 | 18 2 | -------------------------------------------------------------------------------- /.ruby-version: -------------------------------------------------------------------------------- 1 | 2.7.6 2 | -------------------------------------------------------------------------------- /android/app/src/main/res/values-night/colors.xml: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ios/Podfile.properties.json: -------------------------------------------------------------------------------- 1 | { 2 | "expo.jsEngine": "jsc" 3 | } 4 | -------------------------------------------------------------------------------- /.bundle/config: -------------------------------------------------------------------------------- 1 | BUNDLE_PATH: "vendor/bundle" 2 | 2 BUNDLE_FORCE_RUBY_PLATFORM: 1 3 | -------------------------------------------------------------------------------- /docs/star.webm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TamoTamApp/tamotam-app/HEAD/docs/star.webm -------------------------------------------------------------------------------- /assets/images/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TamoTamApp/tamotam-app/HEAD/assets/images/icon.png -------------------------------------------------------------------------------- /assets/images/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TamoTamApp/tamotam-app/HEAD/assets/images/favicon.png -------------------------------------------------------------------------------- /android/app/debug.keystore: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TamoTamApp/tamotam-app/HEAD/android/app/debug.keystore -------------------------------------------------------------------------------- /assets/images/no-image.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TamoTamApp/tamotam-app/HEAD/assets/images/no-image.jpeg -------------------------------------------------------------------------------- /docs/AppleAppStoreButton.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TamoTamApp/tamotam-app/HEAD/docs/AppleAppStoreButton.png -------------------------------------------------------------------------------- /docs/GooglePlayStoreButton.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TamoTamApp/tamotam-app/HEAD/docs/GooglePlayStoreButton.png -------------------------------------------------------------------------------- /docs/dataFlowArchitecture.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TamoTamApp/tamotam-app/HEAD/docs/dataFlowArchitecture.png -------------------------------------------------------------------------------- /interfaces/coordinate.tsx: -------------------------------------------------------------------------------- 1 | export interface Coordinate { 2 | latitude: number; 3 | longitude: number; 4 | } 5 | -------------------------------------------------------------------------------- /assets/fonts/Boiling-BlackDemo.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TamoTamApp/tamotam-app/HEAD/assets/fonts/Boiling-BlackDemo.ttf -------------------------------------------------------------------------------- /assets/fonts/SpaceMono-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TamoTamApp/tamotam-app/HEAD/assets/fonts/SpaceMono-Regular.ttf -------------------------------------------------------------------------------- /ios/TamoTam/Images.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "expo" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /assets/images/icon-map-user-event.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TamoTamApp/tamotam-app/HEAD/assets/images/icon-map-user-event.png -------------------------------------------------------------------------------- /assets/images/icon-map-tamotam-event.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TamoTamApp/tamotam-app/HEAD/assets/images/icon-map-tamotam-event.png -------------------------------------------------------------------------------- /android/gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TamoTamApp/tamotam-app/HEAD/android/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "expo/tsconfig.base", 3 | "compilerOptions": { 4 | "strict": true, 5 | "jsx": "react", 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /ios/TamoTam/noop-file.swift: -------------------------------------------------------------------------------- 1 | // 2 | // @generated 3 | // A blank Swift file must be created for native modules with Swift files to work correctly. 4 | // 5 | -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TamoTamApp/tamotam-app/HEAD/android/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TamoTamApp/tamotam-app/HEAD/android/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TamoTamApp/tamotam-app/HEAD/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TamoTamApp/tamotam-app/HEAD/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TamoTamApp/tamotam-app/HEAD/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /interfaces/region.tsx: -------------------------------------------------------------------------------- 1 | export interface Region { 2 | latitude: number; 3 | longitude: number; 4 | latitudeDelta: number; 5 | longitudeDelta: number; 6 | } 7 | -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TamoTamApp/tamotam-app/HEAD/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TamoTamApp/tamotam-app/HEAD/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TamoTamApp/tamotam-app/HEAD/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TamoTamApp/tamotam-app/HEAD/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /ios/TamoTam/Images.xcassets/SplashScreen.imageset/image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TamoTamApp/tamotam-app/HEAD/ios/TamoTam/Images.xcassets/SplashScreen.imageset/image.png -------------------------------------------------------------------------------- /.prettierrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | arrowParens: 'avoid', 3 | bracketSameLine: true, 4 | bracketSpacing: false, 5 | singleQuote: true, 6 | trailingComma: 'all', 7 | }; 8 | -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-hdpi/splashscreen_image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TamoTamApp/tamotam-app/HEAD/android/app/src/main/res/drawable-hdpi/splashscreen_image.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-mdpi/splashscreen_image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TamoTamApp/tamotam-app/HEAD/android/app/src/main/res/drawable-mdpi/splashscreen_image.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-xhdpi/splashscreen_image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TamoTamApp/tamotam-app/HEAD/android/app/src/main/res/drawable-xhdpi/splashscreen_image.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TamoTamApp/tamotam-app/HEAD/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /ios/TamoTam/AppDelegate.h: -------------------------------------------------------------------------------- 1 | #import 2 | #import 3 | #import 4 | 5 | @interface AppDelegate : EXAppDelegateWrapper 6 | 7 | @end 8 | -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-xxhdpi/splashscreen_image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TamoTamApp/tamotam-app/HEAD/android/app/src/main/res/drawable-xxhdpi/splashscreen_image.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-xxxhdpi/splashscreen_image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TamoTamApp/tamotam-app/HEAD/android/app/src/main/res/drawable-xxxhdpi/splashscreen_image.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TamoTamApp/tamotam-app/HEAD/android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TamoTamApp/tamotam-app/HEAD/android/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TamoTamApp/tamotam-app/HEAD/android/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TamoTamApp/tamotam-app/HEAD/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TamoTamApp/tamotam-app/HEAD/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /ios/TamoTam/Images.xcassets/AppIcon.appiconset/ItunesArtwork@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TamoTamApp/tamotam-app/HEAD/ios/TamoTam/Images.xcassets/AppIcon.appiconset/ItunesArtwork@2x.png -------------------------------------------------------------------------------- /ios/TamoTam/Images.xcassets/AppIcon.appiconset/App-Icon-20x20@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TamoTamApp/tamotam-app/HEAD/ios/TamoTam/Images.xcassets/AppIcon.appiconset/App-Icon-20x20@1x.png -------------------------------------------------------------------------------- /ios/TamoTam/Images.xcassets/AppIcon.appiconset/App-Icon-20x20@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TamoTamApp/tamotam-app/HEAD/ios/TamoTam/Images.xcassets/AppIcon.appiconset/App-Icon-20x20@2x.png -------------------------------------------------------------------------------- /ios/TamoTam/Images.xcassets/AppIcon.appiconset/App-Icon-20x20@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TamoTamApp/tamotam-app/HEAD/ios/TamoTam/Images.xcassets/AppIcon.appiconset/App-Icon-20x20@3x.png -------------------------------------------------------------------------------- /ios/TamoTam/Images.xcassets/AppIcon.appiconset/App-Icon-29x29@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TamoTamApp/tamotam-app/HEAD/ios/TamoTam/Images.xcassets/AppIcon.appiconset/App-Icon-29x29@1x.png -------------------------------------------------------------------------------- /ios/TamoTam/Images.xcassets/AppIcon.appiconset/App-Icon-29x29@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TamoTamApp/tamotam-app/HEAD/ios/TamoTam/Images.xcassets/AppIcon.appiconset/App-Icon-29x29@2x.png -------------------------------------------------------------------------------- /ios/TamoTam/Images.xcassets/AppIcon.appiconset/App-Icon-29x29@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TamoTamApp/tamotam-app/HEAD/ios/TamoTam/Images.xcassets/AppIcon.appiconset/App-Icon-29x29@3x.png -------------------------------------------------------------------------------- /ios/TamoTam/Images.xcassets/AppIcon.appiconset/App-Icon-40x40@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TamoTamApp/tamotam-app/HEAD/ios/TamoTam/Images.xcassets/AppIcon.appiconset/App-Icon-40x40@1x.png -------------------------------------------------------------------------------- /ios/TamoTam/Images.xcassets/AppIcon.appiconset/App-Icon-40x40@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TamoTamApp/tamotam-app/HEAD/ios/TamoTam/Images.xcassets/AppIcon.appiconset/App-Icon-40x40@2x.png -------------------------------------------------------------------------------- /ios/TamoTam/Images.xcassets/AppIcon.appiconset/App-Icon-40x40@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TamoTamApp/tamotam-app/HEAD/ios/TamoTam/Images.xcassets/AppIcon.appiconset/App-Icon-40x40@3x.png -------------------------------------------------------------------------------- /ios/TamoTam/Images.xcassets/AppIcon.appiconset/App-Icon-60x60@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TamoTamApp/tamotam-app/HEAD/ios/TamoTam/Images.xcassets/AppIcon.appiconset/App-Icon-60x60@2x.png -------------------------------------------------------------------------------- /ios/TamoTam/Images.xcassets/AppIcon.appiconset/App-Icon-60x60@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TamoTamApp/tamotam-app/HEAD/ios/TamoTam/Images.xcassets/AppIcon.appiconset/App-Icon-60x60@3x.png -------------------------------------------------------------------------------- /ios/TamoTam/Images.xcassets/AppIcon.appiconset/App-Icon-76x76@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TamoTamApp/tamotam-app/HEAD/ios/TamoTam/Images.xcassets/AppIcon.appiconset/App-Icon-76x76@1x.png -------------------------------------------------------------------------------- /ios/TamoTam/Images.xcassets/AppIcon.appiconset/App-Icon-76x76@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TamoTamApp/tamotam-app/HEAD/ios/TamoTam/Images.xcassets/AppIcon.appiconset/App-Icon-76x76@2x.png -------------------------------------------------------------------------------- /ios/TamoTam/Images.xcassets/SplashScreenBackground.imageset/image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TamoTamApp/tamotam-app/HEAD/ios/TamoTam/Images.xcassets/SplashScreenBackground.imageset/image.png -------------------------------------------------------------------------------- /metro.config.js: -------------------------------------------------------------------------------- 1 | // Learn more https://docs.expo.io/guides/customizing-metro 2 | const { getDefaultConfig } = require('expo/metro-config'); 3 | 4 | module.exports = getDefaultConfig(__dirname); 5 | -------------------------------------------------------------------------------- /android/app/src/main/res/drawable/splashscreen.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /ios/TamoTam/Images.xcassets/AppIcon.appiconset/App-Icon-83.5x83.5@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TamoTamApp/tamotam-app/HEAD/ios/TamoTam/Images.xcassets/AppIcon.appiconset/App-Icon-83.5x83.5@2x.png -------------------------------------------------------------------------------- /android/.gitignore: -------------------------------------------------------------------------------- 1 | # OSX 2 | # 3 | .DS_Store 4 | 5 | # Android/IntelliJ 6 | # 7 | build/ 8 | .idea 9 | .gradle 10 | local.properties 11 | *.iml 12 | *.hprof 13 | 14 | # Bundle artifacts 15 | *.jsbundle 16 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = function (api) { 2 | api.cache(true); 3 | return { 4 | presets: ["babel-preset-expo"], 5 | plugins: ["react-native-reanimated/plugin", "module:react-native-dotenv"], 6 | }; 7 | }; 8 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | # You may use http://rbenv.org/ or https://rvm.io/ to install and use this version 4 | ruby File.read(File.join(__dir__, '.ruby-version')).strip 5 | 6 | gem 'cocoapods', '~> 1.11', '>= 1.11.3' 7 | -------------------------------------------------------------------------------- /android/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-7.6-all.zip 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | -------------------------------------------------------------------------------- /ios/TamoTam/main.m: -------------------------------------------------------------------------------- 1 | #import 2 | 3 | #import "AppDelegate.h" 4 | 5 | int main(int argc, char * argv[]) { 6 | @autoreleasepool { 7 | return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class])); 8 | } 9 | } 10 | 11 | -------------------------------------------------------------------------------- /android/app/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | #ffffff 3 | #ffffff 4 | #023c69 5 | #ffffff 6 | -------------------------------------------------------------------------------- /interfaces/event.tsx: -------------------------------------------------------------------------------- 1 | export interface Event { 2 | id: number | string; 3 | date: Date; 4 | description: string; 5 | firestoreDocumentId?: string; 6 | imageUrl: string; 7 | isUserEvent: boolean; 8 | latitude: number; 9 | longitude: number; 10 | title: string; 11 | } 12 | -------------------------------------------------------------------------------- /android/app/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | TamoTam 3 | contain 4 | false 5 | -------------------------------------------------------------------------------- /constants/Layout.ts: -------------------------------------------------------------------------------- 1 | import { Dimensions } from "react-native"; 2 | 3 | const height = Dimensions.get("window").height; 4 | const width = Dimensions.get("window").width; 5 | 6 | export default { 7 | window: { 8 | height, 9 | width, 10 | }, 11 | isSmallDevice: width < 375, 12 | }; 13 | -------------------------------------------------------------------------------- /components/StyledText.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Text, TextProps } from "./Themed"; 3 | 4 | const StyledText = (props: TextProps) => { 5 | return ( 6 | 7 | ); 8 | }; 9 | 10 | export default StyledText; 11 | -------------------------------------------------------------------------------- /ios/TamoTam/TamoTam.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | aps-environment 6 | development 7 | 8 | -------------------------------------------------------------------------------- /ios/TamoTam.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /ios/TamoTam.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /components/TabBarIcon.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import MaterialCommunityIcons from "react-native-vector-icons/MaterialCommunityIcons"; 3 | 4 | const TabBarIcon = (props: { color: string; name: string }) => { 5 | return ; 6 | }; 7 | 8 | export default TabBarIcon; 9 | -------------------------------------------------------------------------------- /.expo-shared/assets.json: -------------------------------------------------------------------------------- 1 | { 2 | "e997a5256149a4b76e6bfd6cbf519c5e5a0f1d278a3d8fa1253022b03c90473b": true, 3 | "af683c96e0ffd2cf81287651c9433fa44debc1220ca7cb431fe482747f34a505": true, 4 | "12bb71342c6255bbf50437ec8f4441c083f47cdb74bd89160c15e4f43e52a1cb": true, 5 | "40b842e832070c58deac6aa9e08fa459302ee3f9da492c7e77d93d2fbf4a56fd": true 6 | } 7 | -------------------------------------------------------------------------------- /components/__tests__/StyledText-test.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import renderer from "react-test-renderer"; 3 | 4 | import { MonoText } from "../StyledText"; 5 | 6 | it(`renders correctly`, () => { 7 | const tree = renderer.create(Snapshot test!).toJSON(); 8 | 9 | expect(tree).toMatchSnapshot(); 10 | }); 11 | -------------------------------------------------------------------------------- /android/app/src/debug/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | import 'react-native-gesture-handler'; 2 | import { registerRootComponent } from 'expo'; 3 | 4 | import App from './App'; 5 | 6 | // registerRootComponent calls AppRegistry.registerComponent('main', () => App); 7 | // It also ensures that whether you load the app in Expo Go or in a native build, 8 | // the environment is set up appropriately 9 | registerRootComponent(App); 10 | -------------------------------------------------------------------------------- /ios/TamoTam/Images.xcassets/SplashScreen.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images": [ 3 | { 4 | "idiom": "universal", 5 | "filename": "image.png", 6 | "scale": "1x" 7 | }, 8 | { 9 | "idiom": "universal", 10 | "scale": "2x" 11 | }, 12 | { 13 | "idiom": "universal", 14 | "scale": "3x" 15 | } 16 | ], 17 | "info": { 18 | "version": 1, 19 | "author": "expo" 20 | } 21 | } -------------------------------------------------------------------------------- /navigation/LinkingConfiguration.ts: -------------------------------------------------------------------------------- 1 | import * as Linking from "expo-linking"; 2 | 3 | export default { 4 | config: { 5 | screens: { 6 | Map: { 7 | screens: { 8 | MapScreen: "map", 9 | }, 10 | }, 11 | Saved: { 12 | screens: { 13 | SavedScreen: "saved", 14 | }, 15 | }, 16 | }, 17 | }, 18 | NotFound: "*", 19 | prefixes: [Linking.makeUrl("/")], 20 | }; 21 | -------------------------------------------------------------------------------- /ios/TamoTam/Images.xcassets/SplashScreenBackground.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images": [ 3 | { 4 | "idiom": "universal", 5 | "filename": "image.png", 6 | "scale": "1x" 7 | }, 8 | { 9 | "idiom": "universal", 10 | "scale": "2x" 11 | }, 12 | { 13 | "idiom": "universal", 14 | "scale": "3x" 15 | } 16 | ], 17 | "info": { 18 | "version": 1, 19 | "author": "expo" 20 | } 21 | } -------------------------------------------------------------------------------- /firebase.json: -------------------------------------------------------------------------------- 1 | { 2 | "react-native": { 3 | "crashlytics_debug_enabled": true, 4 | "crashlytics_disable_auto_disabler": true, 5 | "crashlytics_auto_collection_enabled": true, 6 | "crashlytics_is_error_generation_on_js_crash_enabled": true, 7 | "crashlytics_javascript_exception_handler_chaining_enabled": true, 8 | "android_task_executor_maximum_pool_size": 10, 9 | "android_task_executor_keep_alive_seconds": 3 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /types.tsx: -------------------------------------------------------------------------------- 1 | export type RootStackParamList = { 2 | EditEvent: undefined; 3 | EventDetail: undefined; 4 | Root: undefined; 5 | NewEvent: undefined; 6 | NotFound: undefined; 7 | }; 8 | 9 | export type BottomTabParamList = { 10 | Map: undefined; 11 | Saved: undefined; 12 | }; 13 | 14 | export type MapParamList = { 15 | MapScreen: undefined; 16 | }; 17 | 18 | export type SavedParamList = { 19 | Map: undefined; 20 | SavedScreen: undefined; 21 | }; 22 | -------------------------------------------------------------------------------- /ios/.gitignore: -------------------------------------------------------------------------------- 1 | # OSX 2 | # 3 | .DS_Store 4 | 5 | # Xcode 6 | # 7 | build/ 8 | *.pbxuser 9 | !default.pbxuser 10 | *.mode1v3 11 | !default.mode1v3 12 | *.mode2v3 13 | !default.mode2v3 14 | *.perspectivev3 15 | !default.perspectivev3 16 | xcuserdata 17 | *.xccheckout 18 | *.moved-aside 19 | DerivedData 20 | *.hmap 21 | *.ipa 22 | *.xcuserstate 23 | project.xcworkspace 24 | .xcode.env.local 25 | 26 | # Bundle artifacts 27 | *.jsbundle 28 | 29 | # CocoaPods 30 | /Pods/ 31 | -------------------------------------------------------------------------------- /constants/Colors.ts: -------------------------------------------------------------------------------- 1 | const tintColorDark = "#fff"; 2 | const tintColorLight = "#2f95dc"; 3 | 4 | export default { 5 | dark: { 6 | background: "#000", 7 | tabIconDefault: "#ccc", 8 | tabIconSelected: tintColorDark, 9 | text: "#ffbfbf", 10 | tint: tintColorDark, 11 | }, 12 | light: { 13 | background: "#fff", 14 | tabIconDefault: "#ccc", 15 | tabIconSelected: tintColorLight, 16 | text: "#b30000", 17 | tint: tintColorLight, 18 | }, 19 | }; 20 | -------------------------------------------------------------------------------- /hooks/useColorScheme.ts: -------------------------------------------------------------------------------- 1 | import { 2 | useColorScheme as _useColorScheme, 3 | ColorSchemeName, 4 | } from "react-native"; 5 | 6 | // The useColorScheme value is always either light or dark, but the built-in 7 | // type suggests that it can be null. This will not happen in practice, so this 8 | // makes it a bit easier to work with. 9 | export default function useColorScheme(): NonNullable { 10 | return _useColorScheme() as NonNullable; 11 | } 12 | -------------------------------------------------------------------------------- /ios/.xcode.env: -------------------------------------------------------------------------------- 1 | # This `.xcode.env` file is versioned and is used to source the environment 2 | # used when running script phases inside Xcode. 3 | # To customize your local environment, you can create an `.xcode.env.local` 4 | # file that is not versioned. 5 | 6 | # NODE_BINARY variable contains the PATH to the node executable. 7 | # 8 | # Customize the NODE_BINARY variable here. 9 | # For example, to use nvm with brew, add the following line 10 | # . "$(brew --prefix nvm)/nvm.sh" --no-use 11 | export NODE_BINARY=$(command -v node) 12 | -------------------------------------------------------------------------------- /ios/TamoTam/Supporting/Expo.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | EXUpdatesCheckOnLaunch 6 | ALWAYS 7 | EXUpdatesEnabled 8 | 9 | EXUpdatesLaunchWaitMs 10 | 0 11 | EXUpdatesSDKVersion 12 | 48.0.0 13 | EXUpdatesURL 14 | https://exp.host/@tamotam/TamoTam 15 | 16 | -------------------------------------------------------------------------------- /components/MaterialHeaderButton.tsx: -------------------------------------------------------------------------------- 1 | import React, { ReactNode } from "react"; 2 | import { 3 | HeaderButton, 4 | HeaderButtonProps, 5 | } from "react-navigation-header-buttons"; 6 | import { MaterialIcons } from "@expo/vector-icons"; 7 | 8 | const MaterialHeaderButton = ( 9 | props: JSX.IntrinsicAttributes & 10 | JSX.IntrinsicClassAttributes & 11 | Readonly & 12 | Readonly<{ children?: ReactNode }> 13 | ) => { 14 | return ( 15 | 16 | ); 17 | }; 18 | 19 | export default MaterialHeaderButton; 20 | -------------------------------------------------------------------------------- /eas.json: -------------------------------------------------------------------------------- 1 | { 2 | "cli": { 3 | "version": ">= 0.50.0" 4 | }, 5 | "build": { 6 | "development": { 7 | "distribution": "internal", 8 | "android": { 9 | "gradleCommand": ":app:assembleDebug" 10 | }, 11 | "ios": { 12 | "buildConfiguration": "Debug" 13 | } 14 | }, 15 | "preview": { 16 | "distribution": "internal", 17 | "ios": { 18 | "simulator": true 19 | } 20 | }, 21 | "production": { 22 | "android": { 23 | "image": "latest" 24 | } 25 | } 26 | }, 27 | "submit": { 28 | "production": {} 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /android/app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # By default, the flags in this file are appended to flags specified 3 | # in /usr/local/Cellar/android-sdk/24.3.3/tools/proguard/proguard-android.txt 4 | # You can edit the include path and order by changing the proguardFiles 5 | # directive in build.gradle. 6 | # 7 | # For more details, see 8 | # http://developer.android.com/guide/developing/tools/proguard.html 9 | 10 | # react-native-reanimated 11 | -keep class com.swmansion.reanimated.** { *; } 12 | -keep class com.facebook.react.turbomodule.** { *; } 13 | 14 | # Add any project specific keep options here: 15 | -------------------------------------------------------------------------------- /android/settings.gradle: -------------------------------------------------------------------------------- 1 | rootProject.name = 'TamoTam' 2 | 3 | apply from: new File(["node", "--print", "require.resolve('expo/package.json')"].execute(null, rootDir).text.trim(), "../scripts/autolinking.gradle"); 4 | useExpoModules() 5 | 6 | apply from: new File(["node", "--print", "require.resolve('@react-native-community/cli-platform-android/package.json')"].execute(null, rootDir).text.trim(), "../native_modules.gradle"); 7 | applyNativeModulesSettingsGradle(settings) 8 | 9 | include ':app' 10 | includeBuild(new File(["node", "--print", "require.resolve('react-native-gradle-plugin/package.json')"].execute(null, rootDir).text.trim()).getParentFile()) 11 | -------------------------------------------------------------------------------- /android/app/src/release/java/com/tamotam/application/ReactNativeFlipper.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Meta Platforms, Inc. and affiliates. 3 | * 4 | *

This source code is licensed under the MIT license found in the LICENSE file in the root 5 | * directory of this source tree. 6 | */ 7 | package com.tamotam.application; 8 | 9 | import android.content.Context; 10 | import com.facebook.react.ReactInstanceManager; 11 | 12 | /** 13 | * Class responsible of loading Flipper inside your React Native application. This is the release 14 | * flavor of it so it's empty as we don't want to load Flipper. 15 | */ 16 | public class ReactNativeFlipper { 17 | public static void initializeFlipper(Context context, ReactInstanceManager reactInstanceManager) { 18 | // Do nothing as we don't want to initialize Flipper on Release. 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /android/app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 9 | 14 | 17 | -------------------------------------------------------------------------------- /app.json: -------------------------------------------------------------------------------- 1 | { 2 | "expo": { 3 | "name": "TamoTam", 4 | "slug": "TamoTam", 5 | "version": "0.9.5", 6 | "orientation": "portrait", 7 | "icon": "./assets/images/icon.png", 8 | "scheme": "myapp", 9 | "userInterfaceStyle": "automatic", 10 | "splash": { 11 | "image": "./assets/images/icon.png", 12 | "resizeMode": "contain", 13 | "backgroundColor": "#ffffff" 14 | }, 15 | "updates": { 16 | "fallbackToCacheTimeout": 0 17 | }, 18 | "assetBundlePatterns": ["**/*"], 19 | "ios": { 20 | "supportsTablet": true, 21 | "buildNumber": "0.9.5", 22 | "googleServicesFile": "./GoogleService-Info.plist", 23 | "bundleIdentifier": "com.tamotam.application" 24 | }, 25 | "android": { 26 | "adaptiveIcon": { 27 | "foregroundImage": "./assets/images/icon.png", 28 | "backgroundColor": "#ffffff" 29 | }, 30 | "googleServicesFile": "./google-services.json", 31 | "package": "com.tamotam.application", 32 | "versionCode": 95 33 | }, 34 | "web": { 35 | "favicon": "./assets/images/favicon.png" 36 | }, 37 | "plugins": [ 38 | "@react-native-firebase/app", 39 | "@react-native-firebase/crashlytics", 40 | "@react-native-firebase/perf" 41 | ], 42 | "extra": { 43 | "eas": { 44 | "projectId": "7f793288-3c1f-458c-9255-c77e8ed3bd90" 45 | } 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /common/readItemFromStorage.ts: -------------------------------------------------------------------------------- 1 | import analytics from "@react-native-firebase/analytics"; 2 | import crashlytics from "@react-native-firebase/crashlytics"; 3 | import AsyncStorage from '@react-native-async-storage/async-storage'; 4 | import { Alert } from "react-native"; 5 | import { Event } from "../interfaces/event"; 6 | 7 | export const readItemFromStorage: () => Promise = async () => { 8 | let eventsParsed: Event[] = []; 9 | 10 | try { 11 | const eventsInJSONString: any = await AsyncStorage.getItem("EVENTS_ASYNC_STORAGE"); 12 | eventsParsed = JSON.parse(eventsInJSONString); 13 | 14 | analytics().logEvent("custom_log", { 15 | description: "--- Analytics: common -> readItemFromStorage -> try, eventsInJSONString: " + eventsInJSONString, 16 | }); 17 | } catch (error: unknown) { 18 | if (error instanceof Error) { 19 | Alert.alert( 20 | "Error ❌", 21 | "Problem with reading events from a local device.", 22 | [{ text: "Okay" }] 23 | ); 24 | 25 | analytics().logEvent("custom_log", { 26 | description: "--- Analytics: common -> readItemFromStorage -> catch, error: " + error, 27 | }); 28 | crashlytics().recordError(error); 29 | } 30 | } finally { 31 | analytics().logEvent("custom_log", { 32 | description: "--- Analytics: common -> readItemFromStorage -> finally", 33 | }); 34 | } 35 | 36 | return eventsParsed ? eventsParsed : []; 37 | }; 38 | 39 | export default readItemFromStorage; 40 | -------------------------------------------------------------------------------- /common/writeItemToStorage.ts: -------------------------------------------------------------------------------- 1 | import analytics from "@react-native-firebase/analytics"; 2 | import crashlytics from "@react-native-firebase/crashlytics"; 3 | import AsyncStorage from '@react-native-async-storage/async-storage'; 4 | import { Alert } from "react-native"; 5 | import { Event } from "../interfaces/event"; 6 | 7 | export const writeItemToStorage: (eventsToAsyncStorage: Event[]) => Promise = async (eventsToAsyncStorage: Event[]) => { 8 | const eventsInJSONString: string = JSON.stringify(eventsToAsyncStorage); 9 | 10 | try { 11 | await AsyncStorage.setItem("EVENTS_ASYNC_STORAGE", eventsInJSONString); 12 | 13 | analytics().logEvent("custom_log", { 14 | description: "--- Analytics: common -> writeItemToStorage -> try, eventsInJSONString: " + eventsInJSONString, 15 | }); 16 | } catch (error: unknown) { 17 | if (error instanceof Error) { 18 | Alert.alert( 19 | "Error ❌", 20 | "Problem with saving events on a local device to avoid a big load during next application launch.", 21 | [{ text: "Okay" }] 22 | ); 23 | 24 | analytics().logEvent("custom_log", { 25 | description: "--- Analytics: common -> writeItemToStorage -> catch, error: " + error, 26 | }); 27 | crashlytics().recordError(error); 28 | } 29 | } finally { 30 | analytics().logEvent("custom_log", { 31 | description: "--- Analytics: common -> writeItemToStorage -> finally", 32 | }); 33 | } 34 | }; 35 | 36 | export default writeItemToStorage; 37 | -------------------------------------------------------------------------------- /screens/NotFoundScreen.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import StyledText from "../components/StyledText"; 3 | import { useNetInfo, NetInfoState } from "@react-native-community/netinfo"; 4 | import { StyleSheet, Text, TouchableOpacity, View } from "react-native"; 5 | import { RootStackParamList } from "../types"; 6 | import { StackScreenProps } from "@react-navigation/stack"; 7 | 8 | export default function NotFoundScreen({ 9 | navigation, 10 | }: StackScreenProps) { 11 | const internetState: NetInfoState = useNetInfo(); 12 | 13 | if (internetState.isConnected === false) { 14 | return ( 15 | 16 | 17 | Please turn on the Internet to use TamoTam. 18 | 19 | 20 | ); 21 | } 22 | 23 | return ( 24 | 25 | This screen doesn't exist. 26 | navigation.replace("Root")} 28 | style={styles.link} 29 | > 30 | Go to home screen! 31 | 32 | 33 | ); 34 | } 35 | 36 | const styles = StyleSheet.create({ 37 | centered: { 38 | alignItems: "center", 39 | flex: 1, 40 | justifyContent: "center", 41 | }, 42 | link: { 43 | marginTop: 15, 44 | paddingVertical: 15, 45 | }, 46 | linkText: { 47 | color: "#2e78b7", 48 | fontSize: 14, 49 | }, 50 | title: { 51 | fontSize: 20, 52 | fontWeight: "bold", 53 | textAlign: "center", 54 | }, 55 | }); 56 | -------------------------------------------------------------------------------- /hooks/useCachedResources.ts: -------------------------------------------------------------------------------- 1 | import * as Font from "expo-font"; 2 | import * as SplashScreen from "expo-splash-screen"; 3 | import analytics from "@react-native-firebase/analytics"; 4 | import crashlytics from "@react-native-firebase/crashlytics"; 5 | import { useEffect, useState } from "react"; 6 | import { Alert } from "react-native"; 7 | import { Ionicons } from "@expo/vector-icons"; 8 | 9 | export default function useCachedResources() { 10 | const [isLoadingComplete, setLoadingComplete] = useState(false); 11 | 12 | useEffect(() => { 13 | async function loadResourcesAndDataAsync() { 14 | try { 15 | SplashScreen.preventAutoHideAsync(); 16 | 17 | // Load fonts 18 | await Font.loadAsync({ 19 | ...Ionicons.font, 20 | "boiling-demo": require("../assets/fonts/Boiling-BlackDemo.ttf"), 21 | "space-mono": require("../assets/fonts/SpaceMono-Regular.ttf"), 22 | }); 23 | 24 | analytics().logEvent("custom_log", { 25 | description: "--- Analytics: hooks -> useCachedResources -> useEffect[] -> try", 26 | }); 27 | } catch (error: unknown) { 28 | if (error instanceof Error) { 29 | Alert.alert( 30 | "Error ❌", 31 | "Problem with loading a font.", 32 | [{ text: "Okay" }] 33 | ); 34 | 35 | analytics().logEvent("custom_log", { 36 | description: "--- Analytics: hooks -> useCachedResources -> useEffect[] -> catch, error: " + error, 37 | }); 38 | crashlytics().recordError(error); 39 | } 40 | } finally { 41 | analytics().logEvent("custom_log", { 42 | description: "--- Analytics: hooks -> useCachedResources -> useEffect[] -> finally", 43 | }); 44 | setLoadingComplete(true); 45 | SplashScreen.hideAsync(); 46 | } 47 | } 48 | 49 | loadResourcesAndDataAsync(); 50 | }, []); 51 | 52 | return isLoadingComplete; 53 | } 54 | -------------------------------------------------------------------------------- /android/app/src/main/res/drawable/rn_edit_text_material.xml: -------------------------------------------------------------------------------- 1 | 2 | 16 | 21 | 22 | 23 | 32 | 33 | 34 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /components/Themed.tsx: -------------------------------------------------------------------------------- 1 | import analytics from "@react-native-firebase/analytics"; 2 | import useColorScheme from "../hooks/useColorScheme"; 3 | import Colors from "../constants/Colors"; 4 | import React from "react"; 5 | import { Text as DefaultText, View as DefaultView } from "react-native"; 6 | 7 | export function useThemeColor( 8 | props: { light?: string; dark?: string }, 9 | colorName: keyof typeof Colors.light & keyof typeof Colors.dark 10 | ) { 11 | const theme: "light" | "dark" = useColorScheme(); 12 | const colorFromProps: string | undefined = props[theme]; 13 | 14 | analytics().logEvent("custom_log", { 15 | description: "--- Analytics: components -> Themed -> useThemeColor, theme: " + theme, 16 | }); 17 | if (colorFromProps) { 18 | return colorFromProps; 19 | } else { 20 | return Colors[theme][colorName]; 21 | } 22 | } 23 | 24 | type ThemeProps = { 25 | darkColor?: string; 26 | lightColor?: string; 27 | }; 28 | 29 | export type TextProps = ThemeProps & DefaultText["props"]; 30 | export type ViewProps = ThemeProps & DefaultView["props"]; 31 | 32 | // TODO: For now it's not being used, check out how text could be changed not manually, but instead using this function. 33 | export function Text(props: TextProps) { 34 | const { darkColor, lightColor, style, ...otherProps } = props; 35 | const color: string = useThemeColor({ dark: darkColor, light: lightColor }, "text"); 36 | 37 | analytics().logEvent("custom_log", { 38 | description: "--- Analytics: components -> Themed -> Text, color: " + color, 39 | }); 40 | return ; 41 | } 42 | 43 | export function View(props: ViewProps) { 44 | const { darkColor, lightColor, style, ...otherProps } = props; 45 | const backgroundColor: string = useThemeColor( 46 | { dark: darkColor, light: lightColor }, 47 | "background" 48 | ); 49 | 50 | analytics().logEvent("custom_log", { 51 | description: "--- Analytics: components -> Themed -> View, backgroundColor: " + backgroundColor, 52 | }); 53 | return ; 54 | } 55 | -------------------------------------------------------------------------------- /android/gradle.properties: -------------------------------------------------------------------------------- 1 | # Project-wide Gradle settings. 2 | 3 | # IDE (e.g. Android Studio) users: 4 | # Gradle settings configured through the IDE *will override* 5 | # any settings specified in this file. 6 | 7 | # For more details on how to configure your build environment visit 8 | # http://www.gradle.org/docs/current/userguide/build_environment.html 9 | 10 | # Specifies the JVM arguments used for the daemon process. 11 | # The setting is particularly useful for tweaking memory settings. 12 | # Default value: -Xmx512m -XX:MaxMetaspaceSize=256m 13 | org.gradle.jvmargs=-Xmx2048m -XX:MaxMetaspaceSize=512m 14 | 15 | # When configured, Gradle will run in incubating parallel mode. 16 | # This option should only be used with decoupled projects. More details, visit 17 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects 18 | # org.gradle.parallel=true 19 | 20 | # AndroidX package structure to make it clearer which packages are bundled with the 21 | # Android operating system, and which are packaged with your app's APK 22 | # https://developer.android.com/topic/libraries/support-library/androidx-rn 23 | android.useAndroidX=true 24 | 25 | # Automatically convert third-party libraries to use AndroidX 26 | android.enableJetifier=true 27 | android.disableAutomaticComponentCreation=true 28 | # Version of flipper SDK to use with React Native 29 | FLIPPER_VERSION=0.125.0 30 | 31 | # Use this property to specify which architecture you want to build. 32 | # You can also override it from the CLI using 33 | # ./gradlew -PreactNativeArchitectures=x86_64 34 | reactNativeArchitectures=armeabi-v7a,arm64-v8a,x86,x86_64 35 | 36 | # Use this property to enable support to the new architecture. 37 | # This will allow you to use TurboModules and the Fabric render in 38 | # your application. You should enable this flag either if you want 39 | # to write custom TurboModules/Fabric components OR use libraries that 40 | # are providing them. 41 | newArchEnabled=false 42 | 43 | # The hosted JavaScript engine 44 | # Supported values: expo.jsEngine = "hermes" | "jsc" 45 | expo.jsEngine=jsc 46 | 47 | # Enable GIF support in React Native images (~200 B increase) 48 | expo.gif.enabled=true 49 | # Enable webp support in React Native images (~85 KB increase) 50 | expo.webp.enabled=true 51 | # Enable animated webp support (~3.4 MB increase) 52 | # Disabled by default because iOS doesn't support animated webp 53 | expo.webp.animated=false 54 | -------------------------------------------------------------------------------- /android/build.gradle: -------------------------------------------------------------------------------- 1 | // Top-level build file where you can add configuration options common to all sub-projects/modules. 2 | 3 | buildscript { 4 | ext { 5 | buildToolsVersion = findProperty('android.buildToolsVersion') ?: '33.0.0' 6 | minSdkVersion = Integer.parseInt(findProperty('android.minSdkVersion') ?: '21') 7 | compileSdkVersion = Integer.parseInt(findProperty('android.compileSdkVersion') ?: '33') 8 | targetSdkVersion = Integer.parseInt(findProperty('android.targetSdkVersion') ?: '33') 9 | if (findProperty('android.kotlinVersion')) { 10 | kotlinVersion = findProperty('android.kotlinVersion') 11 | } 12 | frescoVersion = findProperty('expo.frescoVersion') ?: '2.5.0' 13 | 14 | // We use NDK 23 which has both M1 support and is the side-by-side NDK version from AGP. 15 | ndkVersion = "23.1.7779620" 16 | } 17 | repositories { 18 | google() 19 | mavenCentral() 20 | 21 | // Fix for react-native-async-storage build issue (details: https://stackoverflow.com/questions/74333132/task-react-native-async-storage-async-storagegeneratedebugrfile-failed). 22 | exclusiveContent { 23 | filter { 24 | includeGroup "com.facebook.react" 25 | } 26 | forRepository { 27 | maven { 28 | url "$rootDir/../node_modules/react-native/android" 29 | } 30 | } 31 | } 32 | } 33 | dependencies { 34 | classpath 'com.google.firebase:firebase-crashlytics-gradle:2.9.4' 35 | classpath 'com.google.firebase:perf-plugin:1.4.2' 36 | classpath 'com.google.gms:google-services:4.3.15' 37 | classpath('com.android.tools.build:gradle:7.4.1') 38 | classpath('com.facebook.react:react-native-gradle-plugin') 39 | } 40 | } 41 | 42 | allprojects { 43 | repositories { 44 | maven { 45 | // All of React Native (JS, Obj-C sources, Android binaries) is installed from npm 46 | url(new File(['node', '--print', "require.resolve('react-native/package.json')"].execute(null, rootDir).text.trim(), '../android')) 47 | } 48 | maven { 49 | // Android JSC is installed from npm 50 | url(new File(['node', '--print', "require.resolve('jsc-android/package.json')"].execute(null, rootDir).text.trim(), '../dist')) 51 | } 52 | 53 | google() 54 | mavenCentral() 55 | maven { url 'https://www.jitpack.io' } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /android/app/src/main/java/com/tamotam/application/MainApplication.java: -------------------------------------------------------------------------------- 1 | package com.tamotam.application; 2 | 3 | import android.app.Application; 4 | import android.content.res.Configuration; 5 | import androidx.annotation.NonNull; 6 | 7 | import com.facebook.react.PackageList; 8 | import com.facebook.react.ReactApplication; 9 | import com.facebook.react.ReactNativeHost; 10 | import com.facebook.react.ReactPackage; 11 | import com.facebook.react.defaults.DefaultNewArchitectureEntryPoint; 12 | import com.facebook.react.defaults.DefaultReactNativeHost; 13 | import com.facebook.soloader.SoLoader; 14 | 15 | import expo.modules.ApplicationLifecycleDispatcher; 16 | import expo.modules.ReactNativeHostWrapper; 17 | 18 | import java.util.List; 19 | 20 | public class MainApplication extends Application implements ReactApplication { 21 | 22 | private final ReactNativeHost mReactNativeHost = 23 | new ReactNativeHostWrapper(this, new DefaultReactNativeHost(this) { 24 | @Override 25 | public boolean getUseDeveloperSupport() { 26 | return BuildConfig.DEBUG; 27 | } 28 | 29 | @Override 30 | protected List getPackages() { 31 | @SuppressWarnings("UnnecessaryLocalVariable") 32 | List packages = new PackageList(this).getPackages(); 33 | // Packages that cannot be autolinked yet can be added manually here, for example: 34 | // packages.add(new MyReactNativePackage()); 35 | return packages; 36 | } 37 | 38 | @Override 39 | protected String getJSMainModuleName() { 40 | return "index"; 41 | } 42 | 43 | @Override 44 | protected boolean isNewArchEnabled() { 45 | return BuildConfig.IS_NEW_ARCHITECTURE_ENABLED; 46 | } 47 | 48 | @Override 49 | protected Boolean isHermesEnabled() { 50 | return BuildConfig.IS_HERMES_ENABLED; 51 | } 52 | }); 53 | 54 | @Override 55 | public ReactNativeHost getReactNativeHost() { 56 | return mReactNativeHost; 57 | } 58 | 59 | @Override 60 | public void onCreate() { 61 | super.onCreate(); 62 | SoLoader.init(this, /* native exopackage */ false); 63 | if (BuildConfig.IS_NEW_ARCHITECTURE_ENABLED) { 64 | // If you opted-in for the New Architecture, we load the native entry point for this app. 65 | DefaultNewArchitectureEntryPoint.load(); 66 | } 67 | ReactNativeFlipper.initializeFlipper(this, getReactNativeHost().getReactInstanceManager()); 68 | ApplicationLifecycleDispatcher.onApplicationCreate(this); 69 | } 70 | 71 | @Override 72 | public void onConfigurationChanged(@NonNull Configuration newConfig) { 73 | super.onConfigurationChanged(newConfig); 74 | ApplicationLifecycleDispatcher.onConfigurationChanged(this, newConfig); 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "scripts": { 3 | "start": "expo start --dev-client", 4 | "android": "expo run:android", 5 | "ios": "expo run:ios", 6 | "web": "expo start --web", 7 | "eject": "expo eject", 8 | "test": "jest --watchAll", 9 | "release:android": "react-native run-android --variant=release", 10 | "release:ios": "react-native run-ios --configuration Release" 11 | }, 12 | "jest": { 13 | "preset": "jest-expo" 14 | }, 15 | "dependencies": { 16 | "@expo/vector-icons": "^13.0.0", 17 | "@react-native-async-storage/async-storage": "~1.17.11", 18 | "@react-native-community/cli-platform-android": "^10.2.0", 19 | "@react-native-community/datetimepicker": "6.7.5", 20 | "@react-native-community/netinfo": "9.3.7", 21 | "@react-native-firebase/analytics": "^17.5.0", 22 | "@react-native-firebase/app": "^17.5.0", 23 | "@react-native-firebase/crashlytics": "^17.5.0", 24 | "@react-native-firebase/firestore": "^17.5.0", 25 | "@react-native-firebase/perf": "^17.5.0", 26 | "@react-native-firebase/storage": "^17.5.0", 27 | "@react-navigation/material-bottom-tabs": "^6.2.15", 28 | "@react-navigation/native": "^6.1.6", 29 | "@react-navigation/stack": "^6.3.16", 30 | "axios": "^1.3.4", 31 | "expo": "^48.0.21", 32 | "expo-asset": "~8.9.1", 33 | "expo-constants": "~14.2.1", 34 | "expo-font": "~11.1.1", 35 | "expo-image-picker": "~14.1.1", 36 | "expo-linking": "~4.0.1", 37 | "expo-location": "~15.1.1", 38 | "expo-splash-screen": "~0.18.1", 39 | "expo-sqlite": "~11.1.1", 40 | "expo-status-bar": "~1.4.2", 41 | "expo-web-browser": "~12.1.1", 42 | "react": "18.2.0", 43 | "react-dom": "18.2.0", 44 | "react-native": "0.71.3", 45 | "react-native-config": "^1.5.0", 46 | "react-native-gesture-handler": "2.9.0", 47 | "react-native-map-clustering": "^3.4.2", 48 | "react-native-maps": "^1.11.3", 49 | "react-native-paper": "^5.4.0", 50 | "react-native-reanimated": "^3.8.1", 51 | "react-native-safe-area-context": "4.5.0", 52 | "react-native-screens": "~3.20.0", 53 | "react-native-vector-icons": "^9.2.0", 54 | "react-native-web": "~0.18.12", 55 | "react-navigation-header-buttons": "^10.0.0", 56 | "react-redux": "^8.0.5", 57 | "redux": "^4.2.1", 58 | "redux-thunk": "^2.4.2" 59 | }, 60 | "devDependencies": { 61 | "@babel/core": "^7.20.0", 62 | "@types/react": "^18.0.28", 63 | "@types/react-native": "~0.71.3", 64 | "@types/react-native-vector-icons": "^6.4.13", 65 | "jest-expo": "^48.0.0", 66 | "react-native-dotenv": "3.4.8", 67 | "typescript": "^4.9.4" 68 | }, 69 | "private": true, 70 | "resolutions": { 71 | "@types/react": "~18.0.24" 72 | }, 73 | "version": "0.9.5", 74 | "name": "TamoTam" 75 | } 76 | -------------------------------------------------------------------------------- /store/actions/apis/predictHqEvents.tsx: -------------------------------------------------------------------------------- 1 | import analytics from '@react-native-firebase/analytics'; 2 | import axios, { AxiosResponse } from 'axios'; 3 | import crashlytics from '@react-native-firebase/crashlytics'; 4 | import readItemFromStorage from '../../../common/readItemFromStorage'; 5 | import writeItemToStorage from '../../../common/writeItemToStorage'; 6 | import { Event } from '../../../interfaces/event'; 7 | import { 8 | PREDICTHQ_ACCESS_TOKEN, 9 | PREDICTHQ_CATEGORIES, 10 | PREDICTHQ_LOCATIONS 11 | // @ts-ignore 12 | } from '@env'; 13 | 14 | export const SET_EVENTS = 'SET_EVENTS'; 15 | 16 | export const fetchPredictHqEvents: () => (dispatch: any) => void = () => { 17 | return async (dispatch: any) => { 18 | let eventsInStorage: Event[] | null | any = await readItemFromStorage(); 19 | 20 | await axios({ 21 | headers: { 22 | Authorization: `Bearer ${PREDICTHQ_ACCESS_TOKEN}`, 23 | Accept: 'application/json', 24 | }, 25 | method: 'GET', 26 | params: { 27 | category: PREDICTHQ_CATEGORIES, 28 | "saved_location.location_id": PREDICTHQ_LOCATIONS, 29 | }, 30 | url: 'https://api.predicthq.com/v1/events/', 31 | }) 32 | .then((response: AxiosResponse) => { 33 | for (const id in response.data.results) { 34 | eventsInStorage.push({ 35 | id: 'predicthq' + response.data.results[id].id, 36 | date: new Date(response.data.results[id].start), 37 | description: response.data.results[id].description, 38 | imageUrl: '', 39 | isUserEvent: false, 40 | latitude: response.data.results[id].location[1], 41 | longitude: response.data.results[id].location[0], 42 | title: response.data.results[id].title, 43 | }); 44 | } 45 | analytics().logEvent('custom_log', { 46 | description: 47 | '--- Analytics: store -> actions -> skiRegEvents -> fetchPredictHqEvents -> try, eventsInStorage: ' + 48 | eventsInStorage, 49 | }); 50 | }) 51 | .catch((error: unknown) => { 52 | if (error instanceof Error) { 53 | analytics().logEvent('custom_log', { 54 | description: 55 | '--- Analytics: store -> actions -> fetchPredictHqEvents -> catch, error: ' + 56 | error, 57 | }); 58 | crashlytics().recordError(error); 59 | } 60 | }) 61 | .finally(() => { 62 | dispatch({ 63 | type: SET_EVENTS, 64 | events: eventsInStorage, 65 | }); 66 | writeItemToStorage(eventsInStorage); 67 | analytics().logEvent('custom_log', { 68 | description: 69 | '--- Analytics: store -> actions -> fetchPredictHqEvents -> finally', 70 | }); 71 | }); 72 | }; 73 | }; 74 | -------------------------------------------------------------------------------- /android/app/src/main/java/com/tamotam/application/MainActivity.java: -------------------------------------------------------------------------------- 1 | package com.tamotam.application; 2 | 3 | import android.os.Build; 4 | import android.os.Bundle; 5 | 6 | import com.facebook.react.ReactActivity; 7 | import com.facebook.react.ReactActivityDelegate; 8 | import com.facebook.react.defaults.DefaultNewArchitectureEntryPoint; 9 | import com.facebook.react.defaults.DefaultReactActivityDelegate; 10 | 11 | import expo.modules.ReactActivityDelegateWrapper; 12 | 13 | public class MainActivity extends ReactActivity { 14 | @Override 15 | protected void onCreate(Bundle savedInstanceState) { 16 | // Set the theme to AppTheme BEFORE onCreate to support 17 | // coloring the background, status bar, and navigation bar. 18 | // This is required for expo-splash-screen. 19 | setTheme(R.style.AppTheme); 20 | super.onCreate(null); 21 | } 22 | 23 | /** 24 | * Returns the name of the main component registered from JavaScript. 25 | * This is used to schedule rendering of the component. 26 | */ 27 | @Override 28 | protected String getMainComponentName() { 29 | return "main"; 30 | } 31 | 32 | /** 33 | * Returns the instance of the {@link ReactActivityDelegate}. Here we use a util class {@link 34 | * DefaultReactActivityDelegate} which allows you to easily enable Fabric and Concurrent React 35 | * (aka React 18) with two boolean flags. 36 | */ 37 | @Override 38 | protected ReactActivityDelegate createReactActivityDelegate() { 39 | return new ReactActivityDelegateWrapper(this, BuildConfig.IS_NEW_ARCHITECTURE_ENABLED, new DefaultReactActivityDelegate( 40 | this, 41 | getMainComponentName(), 42 | // If you opted-in for the New Architecture, we enable the Fabric Renderer. 43 | DefaultNewArchitectureEntryPoint.getFabricEnabled(), // fabricEnabled 44 | // If you opted-in for the New Architecture, we enable Concurrent React (i.e. React 18). 45 | DefaultNewArchitectureEntryPoint.getConcurrentReactEnabled() // concurrentRootEnabled 46 | )); 47 | } 48 | 49 | /** 50 | * Align the back button behavior with Android S 51 | * where moving root activities to background instead of finishing activities. 52 | * @see onBackPressed 53 | */ 54 | @Override 55 | public void invokeDefaultOnBackPressed() { 56 | if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.R) { 57 | if (!moveTaskToBack(false)) { 58 | // For non-root activities, use the default implementation to finish them. 59 | super.invokeDefaultOnBackPressed(); 60 | } 61 | return; 62 | } 63 | 64 | // Use the default back button implementation on Android S 65 | // because it's doing more than {@link Activity#moveTaskToBack} in fact. 66 | super.invokeDefaultOnBackPressed(); 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /components/EventItem.tsx: -------------------------------------------------------------------------------- 1 | import useColorScheme from "../hooks/useColorScheme"; 2 | import Colors from "../constants/Colors"; 3 | import React from "react"; 4 | import { Avatar, Button, Card, Paragraph } from "react-native-paper"; 5 | 6 | const LeftContent = (props: any) => ( 7 | 18 | ); 19 | 20 | const EventItem = (props: any) => { 21 | return ( 22 | 30 | 38 | 39 | 47 | {props.description} 48 | 49 | 57 | 🗓️{" "} 58 | { 59 | new Date(props.date) instanceof Date 60 | ? new Date(props.date).toLocaleDateString() 61 | : "No information" 62 | } 63 | 64 | 72 | 🕒{" "} 73 | { 74 | new Date(props.date) instanceof Date 75 | ? new Date(props.date).toLocaleTimeString([], { 76 | hour: "2-digit", 77 | minute: "2-digit", 78 | }) 79 | : "No information" 80 | } 81 | 82 | 83 | 90 | 91 | {props.children} 92 | 93 | 94 | ); 95 | }; 96 | 97 | export default EventItem; 98 | -------------------------------------------------------------------------------- /store/actions/apis/seatGeekEvents.tsx: -------------------------------------------------------------------------------- 1 | import analytics from '@react-native-firebase/analytics'; 2 | import axios, { AxiosResponse } from 'axios'; 3 | import crashlytics from '@react-native-firebase/crashlytics'; 4 | import readItemFromStorage from '../../../common/readItemFromStorage'; 5 | import writeItemToStorage from '../../../common/writeItemToStorage'; 6 | import { Dispatch } from 'redux'; 7 | import { Event } from '../../../interfaces/event'; 8 | import { 9 | SEATGEEK_CLIENT_ID, 10 | SEATGEEK_PAGE_SIZE, 11 | SEATGEEK_NUMBER_OF_PAGES, 12 | SEATGEEK_SECRET, 13 | // @ts-ignore 14 | } from '@env'; 15 | 16 | export const SET_EVENTS = 'SET_EVENTS'; 17 | 18 | export const fetchSeatGeekEvents: () => (dispatch: Dispatch) => void = () => { 19 | return async (dispatch: Dispatch) => { 20 | let eventsInStorage: Event[] | null | any = await readItemFromStorage(); 21 | 22 | for (let page: number = 0; page < SEATGEEK_NUMBER_OF_PAGES; page++) { 23 | await axios({ 24 | method: 'GET', 25 | url: `https://api.seatgeek.com/2/events?client_id=${SEATGEEK_CLIENT_ID}&client_secret=${SEATGEEK_SECRET}&per_page=${SEATGEEK_PAGE_SIZE}`, 26 | }) 27 | .then((response: AxiosResponse) => { 28 | for (const id in response.data.events) { 29 | eventsInStorage.push({ 30 | id: 'seatgeek' + response.data.events[id].id, 31 | date: new Date(response.data.events[id].datetime_local), 32 | description: response.data.events[id].description, 33 | imageUrl: response.data.events[id].performers[0].image, 34 | latitude: response.data.events[id].venue.location.lat, 35 | longitude: response.data.events[id].venue.location.lon, 36 | isUserEvent: false, 37 | title: response.data.events[id].title, 38 | }); 39 | } 40 | analytics().logEvent('custom_log', { 41 | description: 42 | '--- Analytics: store -> actions -> seatGeekEvents -> fetchSeatGeekEvents -> try, eventsInStorage: ' + 43 | eventsInStorage, 44 | }); 45 | }) 46 | .catch((error: unknown) => { 47 | if (error instanceof Error) { 48 | analytics().logEvent('custom_log', { 49 | description: 50 | '--- Analytics: store -> actions -> seatGeekEvents -> fetchSeatGeekEvents -> catch, error: ' + 51 | error, 52 | }); 53 | crashlytics().recordError(error); 54 | } 55 | }) 56 | .finally(() => { 57 | analytics().logEvent('custom_log', { 58 | description: 59 | '--- Analytics: store -> actions -> seatGeekEvents -> fetchSeatGeekEvents -> finally', 60 | }); 61 | }); 62 | } 63 | dispatch({ 64 | type: SET_EVENTS, 65 | events: eventsInStorage, 66 | }); 67 | writeItemToStorage(eventsInStorage); 68 | }; 69 | }; 70 | -------------------------------------------------------------------------------- /android/app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /store/actions/apis/triRegEvents.tsx: -------------------------------------------------------------------------------- 1 | import analytics from '@react-native-firebase/analytics'; 2 | import axios, { AxiosResponse } from 'axios'; 3 | import crashlytics from '@react-native-firebase/crashlytics'; 4 | import readItemFromStorage from '../../../common/readItemFromStorage'; 5 | import writeItemToStorage from '../../../common/writeItemToStorage'; 6 | import { Event } from '../../../interfaces/event'; 7 | 8 | export const SET_EVENTS = 'SET_EVENTS'; 9 | 10 | export const fetchTriRegEvents: () => (dispatch: any) => void = () => { 11 | return async (dispatch: any) => { 12 | let eventsInStorage: Event[] | null | any = await readItemFromStorage(); 13 | 14 | await axios({ 15 | method: 'GET', 16 | url: 'https://www.trireg.com/api/search', 17 | }) 18 | .then((response: AxiosResponse) => { 19 | for (const EventId in response.data.MatchingEvents) { 20 | const arrayByDashSignDivider = 21 | response.data.MatchingEvents[EventId].EventDate.match(/\d+/g); 22 | const checkForDash = /-/.test( 23 | response.data.MatchingEvents[EventId].EventDate, 24 | ) 25 | ? -1 26 | : +1; 27 | const dateInMilliseconds = 28 | +arrayByDashSignDivider[0] + 29 | checkForDash * 30 | (arrayByDashSignDivider[1].slice(0, 2) * 3.6e6 + 31 | arrayByDashSignDivider[1].slice(-2) * 6e4); 32 | 33 | eventsInStorage.push({ 34 | id: 'trireg' + response.data.MatchingEvents[EventId].EventId, 35 | date: new Date(dateInMilliseconds), 36 | description: response.data.MatchingEvents[EventId].PresentedBy, 37 | imageUrl: '', 38 | isUserEvent: false, 39 | latitude: response.data.MatchingEvents[EventId].Latitude, 40 | longitude: response.data.MatchingEvents[EventId].Longitude, 41 | title: response.data.MatchingEvents[EventId].EventName, 42 | }); 43 | } 44 | analytics().logEvent('custom_log', { 45 | description: 46 | '--- Analytics: store -> actions -> triRegEvents -> fetchTriRegEvents -> try, eventsInStorage: ' + 47 | eventsInStorage, 48 | }); 49 | }) 50 | .catch((error: unknown) => { 51 | if (error instanceof Error) { 52 | analytics().logEvent('custom_log', { 53 | description: 54 | '--- Analytics: store -> actions -> triRegEvents -> fetchTriRegEvents -> catch, error: ' + 55 | error, 56 | }); 57 | crashlytics().recordError(error); 58 | } 59 | }) 60 | .finally(() => { 61 | dispatch({ 62 | type: SET_EVENTS, 63 | events: eventsInStorage, 64 | }); 65 | writeItemToStorage(eventsInStorage); 66 | analytics().logEvent('custom_log', { 67 | description: 68 | '--- Analytics: store -> actions -> triRegEvents -> fetchTriRegEvents -> finally', 69 | }); 70 | }); 71 | }; 72 | }; 73 | -------------------------------------------------------------------------------- /ios/TamoTam/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleDisplayName 8 | TamoTam 9 | CFBundleExecutable 10 | $(EXECUTABLE_NAME) 11 | CFBundleIdentifier 12 | $(PRODUCT_BUNDLE_IDENTIFIER) 13 | CFBundleInfoDictionaryVersion 14 | 6.0 15 | CFBundleName 16 | $(PRODUCT_NAME) 17 | CFBundlePackageType 18 | $(PRODUCT_BUNDLE_PACKAGE_TYPE) 19 | CFBundleShortVersionString 20 | 0.9.5 21 | CFBundleSignature 22 | ???? 23 | CFBundleURLTypes 24 | 25 | 26 | CFBundleURLSchemes 27 | 28 | myapp 29 | com.tamotam.application 30 | 31 | 32 | 33 | CFBundleURLSchemes 34 | 35 | com.googleusercontent.apps.425706026242-30a8dpl3du56blco2mdcnqtc3icq5gdf 36 | 37 | 38 | 39 | CFBundleVersion 40 | 0.9.5 41 | LSRequiresIPhoneOS 42 | 43 | NSAppTransportSecurity 44 | 45 | NSAllowsArbitraryLoads 46 | 47 | NSExceptionDomains 48 | 49 | localhost 50 | 51 | NSExceptionAllowsInsecureHTTPLoads 52 | 53 | 54 | 55 | 56 | NSCameraUsageDescription 57 | Allow $(PRODUCT_NAME) to access your camera 58 | NSLocationAlwaysAndWhenInUseUsageDescription 59 | Allow $(PRODUCT_NAME) to access your location 60 | NSLocationAlwaysUsageDescription 61 | Allow $(PRODUCT_NAME) to access your location 62 | NSLocationWhenInUseUsageDescription 63 | Allow $(PRODUCT_NAME) to access your location 64 | NSMicrophoneUsageDescription 65 | Allow $(PRODUCT_NAME) to access your microphone 66 | NSPhotoLibraryUsageDescription 67 | Allow $(PRODUCT_NAME) to access your photos 68 | UILaunchStoryboardName 69 | SplashScreen 70 | UIRequiredDeviceCapabilities 71 | 72 | armv7 73 | 74 | UIRequiresFullScreen 75 | 76 | UIStatusBarStyle 77 | UIStatusBarStyleDarkContent 78 | UISupportedInterfaceOrientations 79 | 80 | UIInterfaceOrientationPortrait 81 | UIInterfaceOrientationPortraitUpsideDown 82 | 83 | UIUserInterfaceStyle 84 | Automatic 85 | UIViewControllerBasedStatusBarAppearance 86 | 87 | 88 | 89 | -------------------------------------------------------------------------------- /ios/TamoTam/Images.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images": [ 3 | { 4 | "idiom": "iphone", 5 | "size": "20x20", 6 | "scale": "2x", 7 | "filename": "App-Icon-20x20@2x.png" 8 | }, 9 | { 10 | "idiom": "iphone", 11 | "size": "20x20", 12 | "scale": "3x", 13 | "filename": "App-Icon-20x20@3x.png" 14 | }, 15 | { 16 | "idiom": "iphone", 17 | "size": "29x29", 18 | "scale": "1x", 19 | "filename": "App-Icon-29x29@1x.png" 20 | }, 21 | { 22 | "idiom": "iphone", 23 | "size": "29x29", 24 | "scale": "2x", 25 | "filename": "App-Icon-29x29@2x.png" 26 | }, 27 | { 28 | "idiom": "iphone", 29 | "size": "29x29", 30 | "scale": "3x", 31 | "filename": "App-Icon-29x29@3x.png" 32 | }, 33 | { 34 | "idiom": "iphone", 35 | "size": "40x40", 36 | "scale": "2x", 37 | "filename": "App-Icon-40x40@2x.png" 38 | }, 39 | { 40 | "idiom": "iphone", 41 | "size": "40x40", 42 | "scale": "3x", 43 | "filename": "App-Icon-40x40@3x.png" 44 | }, 45 | { 46 | "idiom": "iphone", 47 | "size": "60x60", 48 | "scale": "2x", 49 | "filename": "App-Icon-60x60@2x.png" 50 | }, 51 | { 52 | "idiom": "iphone", 53 | "size": "60x60", 54 | "scale": "3x", 55 | "filename": "App-Icon-60x60@3x.png" 56 | }, 57 | { 58 | "idiom": "ipad", 59 | "size": "20x20", 60 | "scale": "1x", 61 | "filename": "App-Icon-20x20@1x.png" 62 | }, 63 | { 64 | "idiom": "ipad", 65 | "size": "20x20", 66 | "scale": "2x", 67 | "filename": "App-Icon-20x20@2x.png" 68 | }, 69 | { 70 | "idiom": "ipad", 71 | "size": "29x29", 72 | "scale": "1x", 73 | "filename": "App-Icon-29x29@1x.png" 74 | }, 75 | { 76 | "idiom": "ipad", 77 | "size": "29x29", 78 | "scale": "2x", 79 | "filename": "App-Icon-29x29@2x.png" 80 | }, 81 | { 82 | "idiom": "ipad", 83 | "size": "40x40", 84 | "scale": "1x", 85 | "filename": "App-Icon-40x40@1x.png" 86 | }, 87 | { 88 | "idiom": "ipad", 89 | "size": "40x40", 90 | "scale": "2x", 91 | "filename": "App-Icon-40x40@2x.png" 92 | }, 93 | { 94 | "idiom": "ipad", 95 | "size": "76x76", 96 | "scale": "1x", 97 | "filename": "App-Icon-76x76@1x.png" 98 | }, 99 | { 100 | "idiom": "ipad", 101 | "size": "76x76", 102 | "scale": "2x", 103 | "filename": "App-Icon-76x76@2x.png" 104 | }, 105 | { 106 | "idiom": "ipad", 107 | "size": "83.5x83.5", 108 | "scale": "2x", 109 | "filename": "App-Icon-83.5x83.5@2x.png" 110 | }, 111 | { 112 | "idiom": "ios-marketing", 113 | "size": "1024x1024", 114 | "scale": "1x", 115 | "filename": "ItunesArtwork@2x.png" 116 | } 117 | ], 118 | "info": { 119 | "version": 1, 120 | "author": "expo" 121 | } 122 | } -------------------------------------------------------------------------------- /store/reducers/events.tsx: -------------------------------------------------------------------------------- 1 | import { Event } from "../../interfaces/event"; 2 | import { 3 | ADD_EVENT, 4 | DELETE_EVENT, 5 | SAVE_EVENT, 6 | SET_EVENTS, 7 | SET_SAVED_EVENTS, 8 | UPDATE_EVENT, 9 | } from "../actions/events"; 10 | 11 | const initialState = { 12 | events: [], 13 | savedEvents: [], 14 | }; 15 | 16 | export default (state = initialState, action: any) => { 17 | switch (action.type) { 18 | case ADD_EVENT: 19 | const newEvent: Event = { 20 | id: action.eventData.id, 21 | date: action.eventData.date, 22 | description: action.eventData.description, 23 | firestoreDocumentId: action.eventData.firestoreDocumentId, 24 | imageUrl: action.eventData.imageUrl, 25 | isUserEvent: action.eventData.isUserEvent, 26 | latitude: action.eventData.latitude, 27 | longitude: action.eventData.longitude, 28 | title: action.eventData.title, 29 | }; 30 | 31 | return { 32 | ...state, 33 | // @ts-ignore 34 | events: state.events.concat(newEvent), 35 | }; 36 | case DELETE_EVENT: 37 | return { 38 | ...state, 39 | savedEvents: state.savedEvents.filter( 40 | (event: Event) => event.id !== action.eventData.id 41 | ), 42 | }; 43 | case SAVE_EVENT: 44 | const savedEvent: Event = { 45 | id: action.eventData.id, 46 | date: action.eventData.date, 47 | description: action.eventData.description, 48 | firestoreDocumentId: action.eventData.firestoreDocumentId, 49 | imageUrl: action.eventData.imageUrl, 50 | isUserEvent: action.eventData.isUserEvent, 51 | latitude: action.eventData.latitude, 52 | longitude: action.eventData.longitude, 53 | title: action.eventData.title, 54 | }; 55 | 56 | return { 57 | ...state, 58 | // @ts-ignore 59 | savedEvents: state.savedEvents.concat(savedEvent), 60 | }; 61 | case SET_EVENTS: 62 | return { 63 | ...state, 64 | events: [...state.events, ...action.events], 65 | }; 66 | case SET_SAVED_EVENTS: 67 | return { 68 | ...state, 69 | savedEvents: action.savedEvents, 70 | }; 71 | case UPDATE_EVENT: 72 | const eventIndex = state.savedEvents.findIndex( 73 | (event: Event) => event.id === action.eventData.id 74 | ); 75 | 76 | const updatedEvent: Event = { 77 | id: action.eventData.id, 78 | date: action.eventData.date, 79 | description: action.eventData.description, 80 | firestoreDocumentId: action.eventData.firestoreDocumentId, 81 | imageUrl: action.eventData.imageUrl, 82 | isUserEvent: action.eventData.isUserEvent, 83 | latitude: action.eventData.latitude, 84 | longitude: action.eventData.longitude, 85 | title: action.eventData.title, 86 | }; 87 | 88 | const updatedEditEvents: Event[] = [...state.savedEvents]; 89 | updatedEditEvents[eventIndex] = updatedEvent; 90 | 91 | return { 92 | ...state, 93 | savedEvents: updatedEditEvents, 94 | }; 95 | default: 96 | return state; 97 | } 98 | }; 99 | -------------------------------------------------------------------------------- /android/gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%"=="" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%"=="" set DIRNAME=. 29 | set APP_BASE_NAME=%~n0 30 | set APP_HOME=%DIRNAME% 31 | 32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 34 | 35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 37 | 38 | @rem Find java.exe 39 | if defined JAVA_HOME goto findJavaFromJavaHome 40 | 41 | set JAVA_EXE=java.exe 42 | %JAVA_EXE% -version >NUL 2>&1 43 | if %ERRORLEVEL% equ 0 goto execute 44 | 45 | echo. 46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 47 | echo. 48 | echo Please set the JAVA_HOME variable in your environment to match the 49 | echo location of your Java installation. 50 | 51 | goto fail 52 | 53 | :findJavaFromJavaHome 54 | set JAVA_HOME=%JAVA_HOME:"=% 55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 56 | 57 | if exist "%JAVA_EXE%" goto execute 58 | 59 | echo. 60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 61 | echo. 62 | echo Please set the JAVA_HOME variable in your environment to match the 63 | echo location of your Java installation. 64 | 65 | goto fail 66 | 67 | :execute 68 | @rem Setup the command line 69 | 70 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 71 | 72 | 73 | @rem Execute Gradle 74 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 75 | 76 | :end 77 | @rem End local scope for the variables with windows NT shell 78 | if %ERRORLEVEL% equ 0 goto mainEnd 79 | 80 | :fail 81 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 82 | rem the _cmd.exe /c_ return code! 83 | set EXIT_CODE=%ERRORLEVEL% 84 | if %EXIT_CODE% equ 0 set EXIT_CODE=1 85 | if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% 86 | exit /b %EXIT_CODE% 87 | 88 | :mainEnd 89 | if "%OS%"=="Windows_NT" endlocal 90 | 91 | :omega 92 | -------------------------------------------------------------------------------- /App.tsx: -------------------------------------------------------------------------------- 1 | import "react-native-gesture-handler"; 2 | import analytics from "@react-native-firebase/analytics"; 3 | import crashlytics from "@react-native-firebase/crashlytics"; 4 | import eventsReducer from "./store/reducers/events"; 5 | import useCachedResources from "./hooks/useCachedResources"; 6 | import useColorScheme from "./hooks/useColorScheme"; 7 | import Colors from "./constants/Colors"; 8 | import React, { useEffect } from "react"; 9 | import Navigation from "./navigation"; 10 | import ReduxThunk from "redux-thunk"; 11 | import { applyMiddleware, createStore, combineReducers } from "redux"; 12 | import { init } from "./helpers/sqlite_db"; 13 | import { useNetInfo, NetInfoState } from "@react-native-community/netinfo"; 14 | import { Alert, StyleSheet } from "react-native"; 15 | import { ColorSchemeName } from "react-native"; 16 | import { StatusBar } from "expo-status-bar"; 17 | import { ActivityIndicator, MD3DarkTheme, Provider as PaperProvider } from "react-native-paper"; 18 | import { Provider as StoreProvider } from "react-redux"; 19 | import { View } from "./components/Themed"; 20 | 21 | init() 22 | .then(() => { 23 | analytics().logEvent("custom_log", { 24 | description: "--- Analytics: App -> init -> then", 25 | }); 26 | }) 27 | .catch((error: unknown) => { 28 | if (error instanceof Error) { 29 | analytics().logEvent("custom_log", { 30 | description: "--- Analytics: App -> init -> catch, error: " + error, 31 | }); 32 | crashlytics().recordError(error); 33 | } 34 | }).finally(() => { 35 | analytics().logEvent("custom_log", { 36 | description: "--- Analytics: App -> init -> finally", 37 | }); 38 | }); 39 | 40 | const rootReducer = combineReducers({ 41 | events: eventsReducer, 42 | }); 43 | 44 | const store = createStore(rootReducer, applyMiddleware(ReduxThunk)); 45 | 46 | export default function App() { 47 | const colorScheme: ColorSchemeName = useColorScheme(); 48 | const internetState: NetInfoState = useNetInfo(); 49 | const isLoadingComplete: boolean = useCachedResources(); 50 | 51 | useEffect(() => { 52 | if (internetState.isConnected === false) { 53 | Alert.alert( 54 | "No Internet! ❌", 55 | "Sorry, we need an Internet connection for TamoTam to run correctly.", 56 | [{ text: "Okay" }] 57 | ); 58 | } 59 | analytics().logEvent("custom_log", { 60 | description: "--- Analytics: App -> useEffect[internetState.isConnected]: " + internetState.isConnected, 61 | }); 62 | }, [internetState.isConnected]); 63 | 64 | if (!isLoadingComplete) { 65 | return ( 66 | 67 | 71 | 72 | ); 73 | } else { 74 | return ( 75 | 76 | 77 | 78 | 79 | 80 | 81 | ); 82 | } 83 | } 84 | 85 | const styles = StyleSheet.create({ 86 | centered: { 87 | alignItems: "center", 88 | flex: 1, 89 | justifyContent: "center", 90 | }, 91 | }); 92 | -------------------------------------------------------------------------------- /store/actions/apis/runRegEvents.tsx: -------------------------------------------------------------------------------- 1 | import analytics from '@react-native-firebase/analytics'; 2 | import axios, { AxiosResponse } from 'axios'; 3 | import crashlytics from '@react-native-firebase/crashlytics'; 4 | import readItemFromStorage from '../../../common/readItemFromStorage'; 5 | import writeItemToStorage from '../../../common/writeItemToStorage'; 6 | import { Event } from '../../../interfaces/event'; 7 | import { 8 | RUNREG_NUMBER_OF_PAGES, 9 | // @ts-ignore 10 | } from '@env'; 11 | 12 | export const SET_EVENTS = 'SET_EVENTS'; 13 | 14 | export const fetchRunRegEvents: () => (dispatch: any) => void = () => { 15 | return async (dispatch: any) => { 16 | let eventsInStorage: Event[] | null | any = await readItemFromStorage(); 17 | 18 | for (let page: number = 0; page < RUNREG_NUMBER_OF_PAGES; page++) { 19 | await axios({ 20 | method: 'GET', 21 | url: `https://www.runreg.com/api/search?startpage=${page}`, 22 | }) 23 | .then((response: AxiosResponse) => { 24 | for (const EventId in response.data.MatchingEvents) { 25 | const arrayByDashSignDivider = 26 | response.data.MatchingEvents[EventId].EventDate.match(/\d+/g); 27 | const checkForDash = /-/.test( 28 | response.data.MatchingEvents[EventId].EventDate, 29 | ) 30 | ? -1 31 | : +1; 32 | const dateInMilliseconds = 33 | +arrayByDashSignDivider[0] + 34 | checkForDash * 35 | (arrayByDashSignDivider[1].slice(0, 2) * 3.6e6 + 36 | arrayByDashSignDivider[1].slice(-2) * 6e4); 37 | 38 | eventsInStorage.push({ 39 | id: 'runreg' + response.data.MatchingEvents[EventId].EventId, 40 | date: new Date(dateInMilliseconds), 41 | description: response.data.MatchingEvents[EventId].PresentedBy, 42 | imageUrl: '', 43 | isUserEvent: false, 44 | latitude: response.data.MatchingEvents[EventId].Latitude, 45 | longitude: response.data.MatchingEvents[EventId].Longitude, 46 | title: response.data.MatchingEvents[EventId].EventName, 47 | }); 48 | } 49 | analytics().logEvent('custom_log', { 50 | description: 51 | '--- Analytics: store -> actions -> runRegEvents -> fetchRunRegEvents -> try, eventsInStorage: ' + 52 | eventsInStorage, 53 | }); 54 | }) 55 | .catch((error: unknown) => { 56 | if (error instanceof Error) { 57 | analytics().logEvent('custom_log', { 58 | description: 59 | '--- Analytics: store -> actions -> runRegEvents -> fetchRunRegEvents -> catch, error: ' + 60 | error, 61 | }); 62 | crashlytics().recordError(error); 63 | } 64 | }) 65 | .finally(() => { 66 | analytics().logEvent('custom_log', { 67 | description: 68 | '--- Analytics: store -> actions -> runRegEvents -> fetchRunRegEvents -> finally', 69 | }); 70 | }); 71 | } 72 | dispatch({ 73 | type: SET_EVENTS, 74 | events: eventsInStorage, 75 | }); 76 | writeItemToStorage(eventsInStorage); 77 | }; 78 | }; 79 | -------------------------------------------------------------------------------- /store/actions/apis/bikeRegEvents.tsx: -------------------------------------------------------------------------------- 1 | import analytics from '@react-native-firebase/analytics'; 2 | import axios, { AxiosResponse } from 'axios'; 3 | import crashlytics from '@react-native-firebase/crashlytics'; 4 | import readItemFromStorage from '../../../common/readItemFromStorage'; 5 | import writeItemToStorage from '../../../common/writeItemToStorage'; 6 | import { Event } from '../../../interfaces/event'; 7 | import { 8 | BIKEREG_NUMBER_OF_PAGES, 9 | // @ts-ignore 10 | } from '@env'; 11 | 12 | export const SET_EVENTS = 'SET_EVENTS'; 13 | 14 | export const fetchBikeRegEvents: () => (dispatch: any) => void = () => { 15 | return async (dispatch: any) => { 16 | let eventsInStorage: Event[] | null | any = await readItemFromStorage(); 17 | 18 | for (let page: number = 0; page < BIKEREG_NUMBER_OF_PAGES; page++) { 19 | await axios({ 20 | method: 'GET', 21 | url: `https://www.bikereg.com/api/search?startpage=${page}`, 22 | }) 23 | .then((response: AxiosResponse) => { 24 | for (const EventId in response.data.MatchingEvents) { 25 | const arrayByDashSignDivider = 26 | response.data.MatchingEvents[EventId].EventDate.match(/\d+/g); 27 | const checkForDash = /-/.test( 28 | response.data.MatchingEvents[EventId].EventDate, 29 | ) 30 | ? -1 31 | : +1; 32 | const dateInMilliseconds = 33 | +arrayByDashSignDivider[0] + 34 | checkForDash * 35 | (arrayByDashSignDivider[1].slice(0, 2) * 3.6e6 + 36 | arrayByDashSignDivider[1].slice(-2) * 6e4); 37 | 38 | eventsInStorage.push({ 39 | id: 'bikereg' + response.data.MatchingEvents[EventId].EventId, 40 | date: new Date(dateInMilliseconds), 41 | description: response.data.MatchingEvents[EventId].PresentedBy, 42 | imageUrl: '', 43 | isUserEvent: false, 44 | latitude: response.data.MatchingEvents[EventId].Latitude, 45 | longitude: response.data.MatchingEvents[EventId].Longitude, 46 | title: response.data.MatchingEvents[EventId].EventName, 47 | }); 48 | } 49 | analytics().logEvent('custom_log', { 50 | description: 51 | '--- Analytics: store -> actions -> bikeRegEvents -> fetchBikeRegEvents -> try, eventsInStorage: ' + 52 | eventsInStorage, 53 | }); 54 | }) 55 | .catch((error: unknown) => { 56 | if (error instanceof Error) { 57 | analytics().logEvent('custom_log', { 58 | description: 59 | '--- Analytics: store -> actions -> bikeRegEvents -> fetchBikeRegEvents -> catch, error: ' + 60 | error, 61 | }); 62 | crashlytics().recordError(error); 63 | } 64 | }) 65 | .finally(() => { 66 | analytics().logEvent('custom_log', { 67 | description: 68 | '--- Analytics: store -> actions -> bikeRegEvents -> fetchBikeRegEvents -> finally', 69 | }); 70 | }); 71 | } 72 | dispatch({ 73 | type: SET_EVENTS, 74 | events: eventsInStorage, 75 | }); 76 | writeItemToStorage(eventsInStorage); 77 | }; 78 | }; 79 | -------------------------------------------------------------------------------- /store/actions/apis/skiRegEvents.tsx: -------------------------------------------------------------------------------- 1 | import analytics from '@react-native-firebase/analytics'; 2 | import axios, { AxiosResponse } from 'axios'; 3 | import crashlytics from '@react-native-firebase/crashlytics'; 4 | import readItemFromStorage from '../../../common/readItemFromStorage'; 5 | import writeItemToStorage from '../../../common/writeItemToStorage'; 6 | import { Event } from '../../../interfaces/event'; 7 | import { 8 | SKIREG_NUMBER_OF_PAGES, 9 | // @ts-ignore 10 | } from '@env'; 11 | 12 | export const SET_EVENTS = 'SET_EVENTS'; 13 | 14 | export const fetchSkiRegEvents: () => (dispatch: any) => void = () => { 15 | return async (dispatch: any) => { 16 | let eventsInStorage: Event[] | null | any = await readItemFromStorage(); 17 | 18 | for (let page: number = 0; page < SKIREG_NUMBER_OF_PAGES; page++) { 19 | await axios({ 20 | method: 'GET', 21 | url: `https://www.skireg.com/api/search/search?startpage=${page}`, 22 | }) 23 | .then((response: AxiosResponse) => { 24 | for (const EventId in response.data.MatchingEvents) { 25 | const arrayByDashSignDivider = 26 | response.data.MatchingEvents[EventId].EventDate.match(/\d+/g); 27 | const checkForDash = /-/.test( 28 | response.data.MatchingEvents[EventId].EventDate, 29 | ) 30 | ? -1 31 | : +1; 32 | const dateInMilliseconds = 33 | +arrayByDashSignDivider[0] + 34 | checkForDash * 35 | (arrayByDashSignDivider[1].slice(0, 2) * 3.6e6 + 36 | arrayByDashSignDivider[1].slice(-2) * 6e4); 37 | 38 | eventsInStorage.push({ 39 | id: 'skireg' + response.data.MatchingEvents[EventId].EventId, 40 | date: new Date(dateInMilliseconds), 41 | description: response.data.MatchingEvents[EventId].PresentedBy, 42 | imageUrl: '', 43 | isUserEvent: false, 44 | latitude: response.data.MatchingEvents[EventId].Latitude, 45 | longitude: response.data.MatchingEvents[EventId].Longitude, 46 | title: response.data.MatchingEvents[EventId].EventName, 47 | }); 48 | } 49 | analytics().logEvent('custom_log', { 50 | description: 51 | '--- Analytics: store -> actions -> skiRegEvents -> fetchSkiRegEvents -> try, eventsInStorage: ' + 52 | eventsInStorage, 53 | }); 54 | }) 55 | .catch((error: unknown) => { 56 | if (error instanceof Error) { 57 | analytics().logEvent('custom_log', { 58 | description: 59 | '--- Analytics: store -> actions -> skiRegEvents -> fetchSkiRegEvents -> catch, error: ' + 60 | error, 61 | }); 62 | crashlytics().recordError(error); 63 | } 64 | }) 65 | .finally(() => { 66 | dispatch({ 67 | type: SET_EVENTS, 68 | events: eventsInStorage, 69 | }); 70 | writeItemToStorage(eventsInStorage); 71 | analytics().logEvent('custom_log', { 72 | description: 73 | '--- Analytics: store -> actions -> skiRegEvents -> fetchSkiRegEvents -> finally', 74 | }); 75 | }); 76 | } 77 | }; 78 | }; 79 | -------------------------------------------------------------------------------- /navigation/index.tsx: -------------------------------------------------------------------------------- 1 | import analytics from "@react-native-firebase/analytics"; 2 | import BottomTabNavigator from "./BottomTabNavigator"; 3 | import EditEventScreen from "../screens/EditEventScreen"; 4 | import EventDetailScreen from "../screens/EventDetailScreen"; 5 | import LinkingConfiguration from "./LinkingConfiguration"; 6 | import NewEventScreen from "../screens/NewEventScreen"; 7 | import NotFoundScreen from "../screens/NotFoundScreen"; 8 | import React, { useRef } from "react"; 9 | import { createStackNavigator } from "@react-navigation/stack"; 10 | import { ColorSchemeName } from "react-native"; 11 | import { 12 | DarkTheme, 13 | DefaultTheme, 14 | NavigationContainer, 15 | } from "@react-navigation/native"; 16 | import { RootStackParamList } from "../types"; 17 | 18 | export default function Navigation({ 19 | colorScheme, 20 | }: { 21 | colorScheme: ColorSchemeName; 22 | }) { 23 | const routeNameRef: React.MutableRefObject = useRef(); 24 | const navigationRef: any = useRef(); 25 | 26 | return ( 27 | { 31 | routeNameRef.current = navigationRef.current.getCurrentRoute().name; 32 | }} 33 | onStateChange={async () => { 34 | const currentRouteName: any = navigationRef.current.getCurrentRoute().name; 35 | const previousRouteName: any = routeNameRef.current; 36 | 37 | if (previousRouteName !== currentRouteName) { 38 | await analytics().logScreenView({ 39 | screen_name: currentRouteName, 40 | screen_class: currentRouteName, 41 | }); 42 | } 43 | routeNameRef.current = currentRouteName; 44 | }} 45 | theme={colorScheme === "dark" ? DarkTheme : DefaultTheme} 46 | > 47 | 48 | 49 | ); 50 | } 51 | 52 | const Stack = createStackNavigator(); 53 | function RootNavigator() { 54 | return ( 55 | 56 | 63 | 73 | 83 | 93 | 103 | 104 | ); 105 | } 106 | -------------------------------------------------------------------------------- /ios/TamoTam.xcodeproj/xcshareddata/xcschemes/TamoTam.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 33 | 39 | 40 | 41 | 42 | 43 | 53 | 55 | 61 | 62 | 63 | 64 | 70 | 72 | 78 | 79 | 80 | 81 | 83 | 84 | 87 | 88 | 89 | -------------------------------------------------------------------------------- /android/app/src/debug/java/com/tamotam/application/ReactNativeFlipper.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Meta Platforms, Inc. and affiliates. 3 | * 4 | *

This source code is licensed under the MIT license found in the LICENSE file in the root 5 | * directory of this source tree. 6 | */ 7 | package com.tamotam.application; 8 | 9 | import android.content.Context; 10 | import com.facebook.flipper.android.AndroidFlipperClient; 11 | import com.facebook.flipper.android.utils.FlipperUtils; 12 | import com.facebook.flipper.core.FlipperClient; 13 | import com.facebook.flipper.plugins.crashreporter.CrashReporterPlugin; 14 | import com.facebook.flipper.plugins.databases.DatabasesFlipperPlugin; 15 | import com.facebook.flipper.plugins.fresco.FrescoFlipperPlugin; 16 | import com.facebook.flipper.plugins.inspector.DescriptorMapping; 17 | import com.facebook.flipper.plugins.inspector.InspectorFlipperPlugin; 18 | import com.facebook.flipper.plugins.network.FlipperOkhttpInterceptor; 19 | import com.facebook.flipper.plugins.network.NetworkFlipperPlugin; 20 | import com.facebook.flipper.plugins.sharedpreferences.SharedPreferencesFlipperPlugin; 21 | import com.facebook.react.ReactInstanceEventListener; 22 | import com.facebook.react.ReactInstanceManager; 23 | import com.facebook.react.bridge.ReactContext; 24 | import com.facebook.react.modules.network.NetworkingModule; 25 | import okhttp3.OkHttpClient; 26 | 27 | /** 28 | * Class responsible of loading Flipper inside your React Native application. This is the debug 29 | * flavor of it. Here you can add your own plugins and customize the Flipper setup. 30 | */ 31 | public class ReactNativeFlipper { 32 | public static void initializeFlipper(Context context, ReactInstanceManager reactInstanceManager) { 33 | if (FlipperUtils.shouldEnableFlipper(context)) { 34 | final FlipperClient client = AndroidFlipperClient.getInstance(context); 35 | 36 | client.addPlugin(new InspectorFlipperPlugin(context, DescriptorMapping.withDefaults())); 37 | client.addPlugin(new DatabasesFlipperPlugin(context)); 38 | client.addPlugin(new SharedPreferencesFlipperPlugin(context)); 39 | client.addPlugin(CrashReporterPlugin.getInstance()); 40 | 41 | NetworkFlipperPlugin networkFlipperPlugin = new NetworkFlipperPlugin(); 42 | NetworkingModule.setCustomClientBuilder( 43 | new NetworkingModule.CustomClientBuilder() { 44 | @Override 45 | public void apply(OkHttpClient.Builder builder) { 46 | builder.addNetworkInterceptor(new FlipperOkhttpInterceptor(networkFlipperPlugin)); 47 | } 48 | }); 49 | client.addPlugin(networkFlipperPlugin); 50 | client.start(); 51 | 52 | // Fresco Plugin needs to ensure that ImagePipelineFactory is initialized 53 | // Hence we run if after all native modules have been initialized 54 | ReactContext reactContext = reactInstanceManager.getCurrentReactContext(); 55 | if (reactContext == null) { 56 | reactInstanceManager.addReactInstanceEventListener( 57 | new ReactInstanceEventListener() { 58 | @Override 59 | public void onReactContextInitialized(ReactContext reactContext) { 60 | reactInstanceManager.removeReactInstanceEventListener(this); 61 | reactContext.runOnNativeModulesQueueThread( 62 | new Runnable() { 63 | @Override 64 | public void run() { 65 | client.addPlugin(new FrescoFlipperPlugin()); 66 | } 67 | }); 68 | } 69 | }); 70 | } else { 71 | client.addPlugin(new FrescoFlipperPlugin()); 72 | } 73 | } 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /ios/TamoTam/AppDelegate.mm: -------------------------------------------------------------------------------- 1 | #import "AppDelegate.h" 2 | #import 3 | 4 | #import 5 | #import 6 | 7 | #import "RNCConfig.h" 8 | #import 9 | 10 | @implementation AppDelegate 11 | 12 | - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions 13 | { 14 | NSString *mapsApiKey = [RNCConfig envFor:@"GOOGLE_MAPS_API"]; 15 | [GMSServices provideAPIKey:mapsApiKey]; 16 | 17 | // @generated begin @react-native-firebase/app-didFinishLaunchingWithOptions-fallback - expo prebuild (DO NOT MODIFY) sync-ecd111c37e49fdd1ed6354203cd6b1e2a38cccda 18 | [FIRApp configure]; 19 | // @generated end @react-native-firebase/app-didFinishLaunchingWithOptions 20 | self.moduleName = @"main"; 21 | 22 | // You can add your custom initial props in the dictionary below. 23 | // They will be passed down to the ViewController used by React Native. 24 | self.initialProps = @{}; 25 | 26 | return [super application:application didFinishLaunchingWithOptions:launchOptions]; 27 | } 28 | 29 | - (NSURL *)sourceURLForBridge:(RCTBridge *)bridge 30 | { 31 | #if DEBUG 32 | return [[RCTBundleURLProvider sharedSettings] jsBundleURLForBundleRoot:@"index"]; 33 | #else 34 | return [[NSBundle mainBundle] URLForResource:@"main" withExtension:@"jsbundle"]; 35 | #endif 36 | } 37 | 38 | /// This method controls whether the `concurrentRoot`feature of React18 is turned on or off. 39 | /// 40 | /// @see: https://reactjs.org/blog/2022/03/29/react-v18.html 41 | /// @note: This requires to be rendering on Fabric (i.e. on the New Architecture). 42 | /// @return: `true` if the `concurrentRoot` feature is enabled. Otherwise, it returns `false`. 43 | - (BOOL)concurrentRootEnabled 44 | { 45 | return true; 46 | } 47 | 48 | // Linking API 49 | - (BOOL)application:(UIApplication *)application openURL:(NSURL *)url options:(NSDictionary *)options { 50 | return [super application:application openURL:url options:options] || [RCTLinkingManager application:application openURL:url options:options]; 51 | } 52 | 53 | // Universal Links 54 | - (BOOL)application:(UIApplication *)application continueUserActivity:(nonnull NSUserActivity *)userActivity restorationHandler:(nonnull void (^)(NSArray> * _Nullable))restorationHandler { 55 | BOOL result = [RCTLinkingManager application:application continueUserActivity:userActivity restorationHandler:restorationHandler]; 56 | return [super application:application continueUserActivity:userActivity restorationHandler:restorationHandler] || result; 57 | } 58 | 59 | // Explicitly define remote notification delegates to ensure compatibility with some third-party libraries 60 | - (void)application:(UIApplication *)application didRegisterForRemoteNotificationsWithDeviceToken:(NSData *)deviceToken 61 | { 62 | return [super application:application didRegisterForRemoteNotificationsWithDeviceToken:deviceToken]; 63 | } 64 | 65 | // Explicitly define remote notification delegates to ensure compatibility with some third-party libraries 66 | - (void)application:(UIApplication *)application didFailToRegisterForRemoteNotificationsWithError:(NSError *)error 67 | { 68 | return [super application:application didFailToRegisterForRemoteNotificationsWithError:error]; 69 | } 70 | 71 | // Explicitly define remote notification delegates to ensure compatibility with some third-party libraries 72 | - (void)application:(UIApplication *)application didReceiveRemoteNotification:(NSDictionary *)userInfo fetchCompletionHandler:(void (^)(UIBackgroundFetchResult))completionHandler 73 | { 74 | return [super application:application didReceiveRemoteNotification:userInfo fetchCompletionHandler:completionHandler]; 75 | } 76 | 77 | @end 78 | -------------------------------------------------------------------------------- /store/actions/apis/usersEvents.tsx: -------------------------------------------------------------------------------- 1 | import analytics from '@react-native-firebase/analytics'; 2 | import crashlytics from '@react-native-firebase/crashlytics'; 3 | import firestore from '@react-native-firebase/firestore'; 4 | import readItemFromStorage from '../../../common/readItemFromStorage'; 5 | import storage from "@react-native-firebase/storage"; 6 | import writeItemToStorage from '../../../common/writeItemToStorage'; 7 | import { Event } from '../../../interfaces/event'; 8 | import { 9 | FIRESTORE_COLLECTION, 10 | // @ts-ignore 11 | } from '@env'; 12 | 13 | export const SET_EVENTS = 'SET_EVENTS'; 14 | 15 | export const fetchUsersEvents: () => (dispatch: any) => void = () => { 16 | return async (dispatch: any) => { 17 | let eventsInStorage: Event[] | null | any = await readItemFromStorage(); 18 | let imageUrlPath: string = ""; 19 | 20 | await firestore() 21 | .collection(FIRESTORE_COLLECTION) 22 | .get() 23 | .then(querySnapshot => { 24 | querySnapshot.forEach(async documentSnapshot => { 25 | await storage().ref(documentSnapshot.data().imageUrl).getDownloadURL() 26 | .then((imageUrlStorage) => { 27 | imageUrlPath = imageUrlStorage; 28 | 29 | analytics().logEvent('custom_log', { 30 | description: 31 | '--- Analytics: store -> actions -> usersEvents -> fetchUsersEvents -> then -> then2' 32 | }); 33 | }).catch((error: unknown) => { 34 | if (error instanceof Error) { 35 | imageUrlPath = ""; 36 | 37 | analytics().logEvent('custom_log', { 38 | description: 39 | '--- Analytics: store -> actions -> usersEvents -> fetchUsersEvents -> then -> catch, error: ' + 40 | error, 41 | }); 42 | crashlytics().recordError(error); 43 | } 44 | }); 45 | 46 | eventsInStorage.push({ 47 | id: documentSnapshot.data().id, 48 | date: new Date(documentSnapshot.data().date.seconds * 1000), 49 | description: documentSnapshot.data().description, 50 | // @ts-ignore 51 | firestoreDocumentId: documentSnapshot.ref._documentPath._parts[1], 52 | imageUrl: imageUrlPath, 53 | isUserEvent: documentSnapshot.data().isUserEvent, 54 | latitude: documentSnapshot.data().latitude, 55 | longitude: documentSnapshot.data().longitude, 56 | title: documentSnapshot.data().title, 57 | }); 58 | }); 59 | analytics().logEvent('custom_log', { 60 | description: 61 | '--- Analytics: store -> actions -> usersEvents -> fetchUsersEvents -> then, eventsInStorage: ' + 62 | eventsInStorage, 63 | }); 64 | }) 65 | .catch((error: unknown) => { 66 | if (error instanceof Error) { 67 | analytics().logEvent('custom_log', { 68 | description: 69 | '--- Analytics: store -> actions -> usersEvents -> fetchUsersEvents -> catch, error: ' + 70 | error, 71 | }); 72 | crashlytics().recordError(error); 73 | } 74 | }) 75 | .finally(() => { 76 | // TODO: Temporary solution with the "setTimeout", make it properly with async actions (redux-thunk). 77 | setTimeout(() => { 78 | dispatch({ 79 | type: SET_EVENTS, 80 | events: eventsInStorage, 81 | }); 82 | writeItemToStorage(eventsInStorage); 83 | }, 1000); 84 | analytics().logEvent('custom_log', { 85 | description: 86 | '--- Analytics: store -> actions -> usersEvents -> fetchUsersEvents -> finally', 87 | }); 88 | }); 89 | }; 90 | }; 91 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | .pnpm-debug.log* 9 | 10 | # Diagnostic reports (https://nodejs.org/api/report.html) 11 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 12 | 13 | # Runtime data 14 | pids 15 | *.pid 16 | *.seed 17 | *.pid.lock 18 | 19 | # Directory for instrumented libs generated by jscoverage/JSCover 20 | lib-cov 21 | 22 | # Coverage directory used by tools like istanbul 23 | coverage 24 | *.lcov 25 | 26 | # nyc test coverage 27 | .nyc_output 28 | 29 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 30 | .grunt 31 | 32 | # Bower dependency directory (https://bower.io/) 33 | bower_components 34 | 35 | # node-waf configuration 36 | .lock-wscript 37 | 38 | # Compiled binary addons (https://nodejs.org/api/addons.html) 39 | build/Release 40 | 41 | # Dependency directories 42 | node_modules/ 43 | jspm_packages/ 44 | 45 | # Snowpack dependency directory (https://snowpack.dev/) 46 | web_modules/ 47 | 48 | # TypeScript cache 49 | *.tsbuildinfo 50 | 51 | # Optional npm cache directory 52 | .npm 53 | 54 | # Optional eslint cache 55 | .eslintcache 56 | 57 | # Microbundle cache 58 | .rpt2_cache/ 59 | .rts2_cache_cjs/ 60 | .rts2_cache_es/ 61 | .rts2_cache_umd/ 62 | 63 | # Optional REPL history 64 | .node_repl_history 65 | 66 | # Output of 'npm pack' 67 | *.tgz 68 | 69 | # Yarn Integrity file 70 | .yarn-integrity 71 | 72 | # dotenv environment variables file 73 | .env 74 | .env.test 75 | .env.production 76 | 77 | # parcel-bundler cache (https://parceljs.org/) 78 | .cache 79 | .parcel-cache 80 | 81 | # Next.js build output 82 | .next 83 | out 84 | 85 | # Nuxt.js build / generate output 86 | .nuxt 87 | dist 88 | 89 | # Gatsby files 90 | .cache/ 91 | # Comment in the public line in if your project uses Gatsby and not Next.js 92 | # https://nextjs.org/blog/next-9-1#public-directory-support 93 | # public 94 | 95 | # vuepress build output 96 | .vuepress/dist 97 | 98 | # Serverless directories 99 | .serverless/ 100 | 101 | # FuseBox cache 102 | .fusebox/ 103 | 104 | # DynamoDB Local files 105 | .dynamodb/ 106 | 107 | # TernJS port file 108 | .tern-port 109 | 110 | # Stores VSCode versions used for testing VSCode extensions 111 | .vscode-test 112 | 113 | # yarn v2 114 | .yarn/cache 115 | .yarn/unplugged 116 | .yarn/build-state.yml 117 | .yarn/install-state.gz 118 | .pnp.* 119 | 120 | # OSX 121 | # 122 | .DS_Store 123 | 124 | # React Native 125 | .expo/ 126 | *.jks 127 | *.p8 128 | *.p12 129 | *.key 130 | *.mobileprovision 131 | *.orig.* 132 | web-build/ 133 | 134 | # Others 135 | google-services.json 136 | GoogleService-Info.plist 137 | 138 | 139 | 140 | # @generated expo-cli sync-647791c5bd841d5c91864afb91c302553d300921 141 | # The following patterns were generated by expo-cli 142 | 143 | # OSX 144 | # 145 | .DS_Store 146 | 147 | # Xcode 148 | # 149 | build/ 150 | *.pbxuser 151 | !default.pbxuser 152 | *.mode1v3 153 | !default.mode1v3 154 | *.mode2v3 155 | !default.mode2v3 156 | *.perspectivev3 157 | !default.perspectivev3 158 | xcuserdata 159 | *.xccheckout 160 | *.moved-aside 161 | DerivedData 162 | *.hmap 163 | *.ipa 164 | *.xcuserstate 165 | project.xcworkspace 166 | 167 | # Android/IntelliJ 168 | # 169 | build/ 170 | .idea 171 | .gradle 172 | local.properties 173 | *.iml 174 | *.hprof 175 | .cxx/ 176 | *.keystore 177 | !debug.keystore 178 | release 179 | 180 | # node.js 181 | # 182 | node_modules/ 183 | npm-debug.log 184 | yarn-error.log 185 | 186 | # Bundle artifacts 187 | *.jsbundle 188 | 189 | # CocoaPods 190 | /ios/Pods/ 191 | 192 | # Temporary files created by Metro to check the health of the file watcher 193 | .metro-health-check* 194 | 195 | # Expo 196 | .expo/ 197 | web-build/ 198 | dist/ 199 | 200 | # @end expo-cli 201 | -------------------------------------------------------------------------------- /store/actions/apis/ticketmasterEvents.tsx: -------------------------------------------------------------------------------- 1 | import analytics from '@react-native-firebase/analytics'; 2 | import axios, { AxiosResponse } from 'axios'; 3 | import crashlytics from '@react-native-firebase/crashlytics'; 4 | import readItemFromStorage from '../../../common/readItemFromStorage'; 5 | import writeItemToStorage from '../../../common/writeItemToStorage'; 6 | import { 7 | TICKETMASTER_API_KEY, 8 | TICKETMASTER_NUMBER_OF_PAGES, 9 | TICKETMASTER_SIZE, 10 | // @ts-ignore 11 | } from '@env'; 12 | import { Event } from '../../../interfaces/event'; 13 | 14 | export const SET_EVENTS = 'SET_EVENTS'; 15 | 16 | export const fetchTicketmasterEvents: () => (dispatch: any) => void = () => { 17 | return async (dispatch: any) => { 18 | let eventsInStorage: Event[] | null | any = await readItemFromStorage(); 19 | let ticketmasterEventsArrayFromSet: Event[] = []; 20 | 21 | const ticketmasterCountries: string[] = [ 22 | 'AT', 23 | 'AU', 24 | 'BE', 25 | 'CA', 26 | 'CH', 27 | 'CZ', 28 | 'DE', 29 | 'DK', 30 | 'ES', 31 | 'FI', 32 | 'GB', 33 | 'IE', 34 | 'LU', 35 | 'MX', 36 | 'NO', 37 | 'NL', 38 | 'PL', 39 | 'PT', 40 | 'SE', 41 | 'US', 42 | ]; 43 | for (const country of ticketmasterCountries) { 44 | for (let page: number = 0; page < TICKETMASTER_NUMBER_OF_PAGES; page++) { 45 | await axios({ 46 | method: 'GET', 47 | url: `https://app.ticketmaster.com/discovery/v2/events.json?countryCode=${country}&apikey=${TICKETMASTER_API_KEY}&size=${TICKETMASTER_SIZE}&page=${page}`, 48 | }) 49 | .then((response: AxiosResponse) => { 50 | for (const id in response.data._embedded.events) { 51 | eventsInStorage.push({ 52 | id: 'ticketmaster' + response.data._embedded.events[id].id, 53 | date: new Date( 54 | response.data._embedded.events[id].dates.start.dateTime, 55 | ), 56 | description: 57 | response.data._embedded.events[id]._embedded.venues[0].city 58 | .name, 59 | imageUrl: response.data._embedded.events[id].images[0].url, 60 | isUserEvent: false, 61 | latitude: 62 | response.data._embedded.events[id]._embedded.venues[0] 63 | .location.latitude, 64 | longitude: 65 | response.data._embedded.events[id]._embedded.venues[0] 66 | .location.longitude, 67 | title: response.data._embedded.events[id].name, 68 | }); 69 | } 70 | analytics().logEvent('custom_log', { 71 | description: 72 | '--- Analytics: store -> actions -> ticketmasterEvents -> fetchTicketmasterEvents -> try, eventsInStorage: ' + 73 | eventsInStorage, 74 | }); 75 | }) 76 | .catch((error: unknown) => { 77 | if (error instanceof Error) { 78 | analytics().logEvent('custom_log', { 79 | description: 80 | '--- Analytics: store -> actions -> ticketmasterEvents -> fetchTicketmasterEvents -> catch, error: ' + 81 | error, 82 | }); 83 | crashlytics().recordError(error); 84 | } 85 | }) 86 | .finally(() => { 87 | analytics().logEvent('custom_log', { 88 | description: 89 | '--- Analytics: store -> actions -> ticketmasterEvents -> fetchTicketmasterEvents -> finally', 90 | }); 91 | }); 92 | } 93 | } 94 | ticketmasterEventsArrayFromSet = Array.from(new Set(eventsInStorage)); 95 | dispatch({ 96 | type: SET_EVENTS, 97 | events: ticketmasterEventsArrayFromSet, 98 | }); 99 | writeItemToStorage(ticketmasterEventsArrayFromSet); 100 | }; 101 | }; 102 | -------------------------------------------------------------------------------- /components/SelectImage.tsx: -------------------------------------------------------------------------------- 1 | import * as ImagePicker from "expo-image-picker"; 2 | import analytics from "@react-native-firebase/analytics"; 3 | import storage, { FirebaseStorageTypes } from "@react-native-firebase/storage"; 4 | import useColorScheme from "../hooks/useColorScheme"; 5 | import Colors from "../constants/Colors"; 6 | import React, { useEffect, useState } from "react"; 7 | import { ColorSchemeName, Image, Platform, StyleSheet } from "react-native"; 8 | import { Button } from "react-native-paper"; 9 | import { Text, View } from "./Themed"; 10 | 11 | const SelectImage = (props: { 12 | existingImageUrl?: string; 13 | imageUrlStorageFromChild: (arg0: string) => void; 14 | }) => { 15 | const colorScheme: ColorSchemeName = useColorScheme(); 16 | const [pickedImage, setPickedImage] = useState(""); 17 | 18 | useEffect(() => { 19 | (async () => { 20 | if (Platform.OS !== "web") { 21 | const { status } = 22 | await ImagePicker.requestMediaLibraryPermissionsAsync(); 23 | 24 | analytics().logEvent("custom_log", { 25 | description: "--- Analytics: components -> SelectImage -> useEffect[], status: " + status, 26 | }); 27 | analytics().logEvent("custom_log", { 28 | description: "--- Analytics: components -> SelectImage -> useEffect[], Platform.OS: " + Platform.OS, 29 | }); 30 | } 31 | })(); 32 | }, []); 33 | 34 | const selectImageHandler = async () => { 35 | let bucketStorageReference: FirebaseStorageTypes.Reference | string = ""; 36 | let image: ImagePicker.ImagePickerResult = 37 | await ImagePicker.launchImageLibraryAsync({ 38 | // TODO: Shall it be also camera? 39 | mediaTypes: ImagePicker.MediaTypeOptions.All, 40 | allowsEditing: true, 41 | aspect: [16, 9], 42 | quality: 1, 43 | }); 44 | 45 | if (!image.canceled) { 46 | setPickedImage(image.assets[0].uri); 47 | 48 | bucketStorageReference = await storage().ref(new Date().getTime().toString()); // Randomize the file name of an image in order not to forward local path of a user image. 49 | 50 | const pathToFile: string = image.assets[0].uri; 51 | const bucketStorageTaskSnapshot: FirebaseStorageTypes.TaskSnapshot | any = await bucketStorageReference.putFile(pathToFile); 52 | 53 | props.imageUrlStorageFromChild(bucketStorageTaskSnapshot.metadata.fullPath); 54 | } 55 | analytics().logEvent("custom_log", { 56 | description: "--- Analytics: components -> SelectImage -> selectImageHandler, image: " + image, 57 | }); 58 | }; 59 | 60 | return ( 61 | 62 | 63 | {!pickedImage && 64 | (!props.existingImageUrl || typeof props.existingImageUrl !== "string") ? ( 65 | No image picked yet. 66 | ) : ( 67 | 75 | )} 76 | 77 | 91 | 92 | ); 93 | }; 94 | 95 | const styles = StyleSheet.create({ 96 | selectImage: { 97 | alignItems: "center", 98 | marginBottom: 15, 99 | marginTop: 15, 100 | }, 101 | imagePreview: { 102 | alignItems: "center", 103 | borderColor: "#ccc", 104 | borderRadius: 10, 105 | borderWidth: 1, 106 | height: 200, 107 | justifyContent: "center", 108 | marginBottom: 10, 109 | width: "100%", 110 | }, 111 | image: { 112 | borderRadius: 10, 113 | height: "100%", 114 | width: "100%", 115 | }, 116 | }); 117 | 118 | export default SelectImage; 119 | -------------------------------------------------------------------------------- /navigation/BottomTabNavigator.tsx: -------------------------------------------------------------------------------- 1 | import useColorScheme from "../hooks/useColorScheme"; 2 | import Colors from "../constants/Colors"; 3 | import MapScreen from "../screens/MapScreen"; 4 | import React, { Fragment } from "react"; 5 | import SavedScreen from "../screens/SavedScreen"; 6 | import TabBarIcon from "../components/TabBarIcon"; 7 | import { createStackNavigator } from "@react-navigation/stack"; 8 | import { createMaterialBottomTabNavigator } from "@react-navigation/material-bottom-tabs"; 9 | import { useIsFocused } from "@react-navigation/native"; 10 | import { BottomTabParamList, MapParamList, SavedParamList } from "../types"; 11 | import { ColorSchemeName } from "react-native"; 12 | import { FAB, Portal } from "react-native-paper"; 13 | 14 | const BottomTab = createMaterialBottomTabNavigator(); 15 | 16 | export default function BottomTabNavigator({ navigation }: any) { 17 | const colorScheme: ColorSchemeName = useColorScheme(); 18 | const isFocused: boolean = useIsFocused(); 19 | 20 | return ( 21 | 22 | 39 | { 44 | let iconName: string = !focused 45 | ? "map-check" 46 | : "map-check-outline"; 47 | 48 | return ; 49 | }, 50 | }} 51 | /> 52 | { 57 | let iconName: string = !focused 58 | ? "bookmark" 59 | : "bookmark-check-outline"; 60 | 61 | return ; 62 | }, 63 | }} 64 | /> 65 | 66 | 67 | navigation.navigate("NewEvent")} 72 | style={{ 73 | backgroundColor: 74 | colorScheme === "dark" 75 | ? Colors.dark.background 76 | : Colors.light.background, 77 | borderWidth: 1, 78 | borderColor: 79 | colorScheme === "dark" ? Colors.dark.text : Colors.light.text, 80 | borderRadius: 50, 81 | bottom: 125, 82 | position: "absolute", 83 | right: 10, 84 | shadowColor: 85 | colorScheme === "dark" ? Colors.dark.text : Colors.light.text, 86 | shadowRadius: 15, 87 | }} 88 | visible={isFocused} 89 | /> 90 | 91 | 92 | ); 93 | } 94 | 95 | const MapStack = createStackNavigator(); 96 | function MapNavigator() { 97 | return ( 98 | 99 | 109 | 110 | ); 111 | } 112 | 113 | const SavedStack = createStackNavigator(); 114 | function SavedNavigator() { 115 | return ( 116 | 117 | 127 | 128 | ); 129 | } 130 | -------------------------------------------------------------------------------- /ios/Podfile: -------------------------------------------------------------------------------- 1 | require File.join(File.dirname(`node --print "require.resolve('expo/package.json')"`), "scripts/autolinking") 2 | require File.join(File.dirname(`node --print "require.resolve('react-native/package.json')"`), "scripts/react_native_pods") 3 | require File.join(File.dirname(`node --print "require.resolve('@react-native-community/cli-platform-ios/package.json')"`), "native_modules") 4 | 5 | require 'json' 6 | podfile_properties = JSON.parse(File.read(File.join(__dir__, 'Podfile.properties.json'))) rescue {} 7 | 8 | ENV['RCT_NEW_ARCH_ENABLED'] = podfile_properties['newArchEnabled'] == 'true' ? '1' : '0' 9 | 10 | platform :ios, podfile_properties['ios.deploymentTarget'] || '13.4' 11 | install! 'cocoapods', 12 | :deterministic_uuids => false 13 | 14 | prepare_react_native_project! 15 | 16 | # If you are using a `react-native-flipper` your iOS build will fail when `NO_FLIPPER=1` is set. 17 | # because `react-native-flipper` depends on (FlipperKit,...), which will be excluded. To fix this, 18 | # you can also exclude `react-native-flipper` in `react-native.config.js` 19 | # 20 | # ```js 21 | # module.exports = { 22 | # dependencies: { 23 | # ...(process.env.NO_FLIPPER ? { 'react-native-flipper': { platforms: { ios: null } } } : {}), 24 | # } 25 | # } 26 | # ``` 27 | flipper_config = FlipperConfiguration.disabled 28 | if ENV['NO_FLIPPER'] == '1' then 29 | # Explicitly disabled through environment variables 30 | flipper_config = FlipperConfiguration.disabled 31 | elsif podfile_properties.key?('ios.flipper') then 32 | # Configure Flipper in Podfile.properties.json 33 | if podfile_properties['ios.flipper'] == 'true' then 34 | flipper_config = FlipperConfiguration.enabled(["Debug", "Release"]) 35 | elsif podfile_properties['ios.flipper'] != 'false' then 36 | flipper_config = FlipperConfiguration.enabled(["Debug", "Release"], { 'Flipper' => podfile_properties['ios.flipper'] }) 37 | end 38 | end 39 | 40 | target 'TamoTam' do 41 | use_expo_modules! 42 | 43 | pod 'react-native-google-maps', :path => '../node_modules/react-native-maps' 44 | pod 'RNCAsyncStorage', :path => '../node_modules/@react-native-async-storage/async-storage' 45 | pod 'Firebase', :modular_headers => true 46 | pod 'FirebaseAppCheckInterop', :modular_headers => true 47 | pod 'FirebaseAuthInterop', :modular_headers => true 48 | pod 'FirebaseCore', :modular_headers => true 49 | pod 'FirebaseCoreExtension', :modular_headers => true 50 | pod 'FirebaseInstallations', :modular_headers => true 51 | pod 'GoogleDataTransport', :modular_headers => true 52 | pod 'GTMSessionFetcher', :modular_headers => true 53 | pod 'nanopb', :modular_headers => true 54 | pod 'GoogleUtilities', :modular_headers => true 55 | use_frameworks! :linkage => :static 56 | $RNFirebaseAsStaticFramework = true 57 | config = use_native_modules! 58 | 59 | use_frameworks! :linkage => podfile_properties['ios.useFrameworks'].to_sym if podfile_properties['ios.useFrameworks'] 60 | use_frameworks! :linkage => ENV['USE_FRAMEWORKS'].to_sym if ENV['USE_FRAMEWORKS'] 61 | 62 | # Flags change depending on the env values. 63 | flags = get_default_flags() 64 | 65 | use_react_native!( 66 | :path => config[:reactNativePath], 67 | :hermes_enabled => podfile_properties['expo.jsEngine'] == nil || podfile_properties['expo.jsEngine'] == 'hermes', 68 | :fabric_enabled => flags[:fabric_enabled], 69 | # An absolute path to your application root. 70 | :app_path => "#{Pod::Config.instance.installation_root}/..", 71 | # Note that if you have use_frameworks! enabled, Flipper will not work if enabled 72 | :flipper_configuration => flipper_config 73 | ) 74 | 75 | post_install do |installer| 76 | react_native_post_install( 77 | installer, 78 | config[:reactNativePath], 79 | # Set `mac_catalyst_enabled` to `true` in order to apply patches 80 | # necessary for Mac Catalyst builds 81 | :mac_catalyst_enabled => false 82 | ) 83 | __apply_Xcode_12_5_M1_post_install_workaround(installer) 84 | 85 | installer.pods_project.build_configurations.each do |config| 86 | config.build_settings["EXCLUDED_ARCHS[sdk=iphonesimulator*]"] = "arm64" 87 | end 88 | 89 | # This is necessary for Xcode 14, because it signs resource bundles by default 90 | # when building for devices. 91 | installer.target_installation_results.pod_target_installation_results 92 | .each do |pod_name, target_installation_result| 93 | target_installation_result.resource_bundle_targets.each do |resource_bundle_target| 94 | resource_bundle_target.build_configurations.each do |config| 95 | config.build_settings['CODE_SIGNING_ALLOWED'] = 'NO' 96 | end 97 | end 98 | end 99 | end 100 | 101 | post_integrate do |installer| 102 | begin 103 | expo_patch_react_imports!(installer) 104 | rescue => e 105 | Pod::UI.warn e 106 | end 107 | end 108 | end 109 | -------------------------------------------------------------------------------- /ios/TamoTam/SplashScreen.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | -------------------------------------------------------------------------------- /screens/EventDetailScreen.tsx: -------------------------------------------------------------------------------- 1 | import useColorScheme from "../hooks/useColorScheme"; 2 | import Colors from "../constants/Colors"; 3 | import CustomMapStyles from "../constants/CustomMapStyles"; 4 | import MapView, { Marker, PROVIDER_GOOGLE } from "react-native-maps"; 5 | import MaterialHeaderButton from "../components/MaterialHeaderButton"; 6 | import React, { useLayoutEffect, useRef, MutableRefObject, useEffect } from "react"; 7 | import StyledText from "../components/StyledText"; 8 | import { useNetInfo, NetInfoState } from "@react-native-community/netinfo"; 9 | import { useSelector } from "react-redux"; 10 | import { ColorSchemeName, Dimensions, Image, ScrollView, StyleSheet } from "react-native"; 11 | import { Coordinate } from "../interfaces/coordinate"; 12 | import { Event } from "../interfaces/event"; 13 | import { HeaderButtons, Item } from "react-navigation-header-buttons"; 14 | import { Region } from "../interfaces/region"; 15 | import { View } from "../components/Themed"; 16 | 17 | export default function EventDetailScreen({ navigation, route }: any) { 18 | const colorScheme: ColorSchemeName = useColorScheme(); 19 | const eventId: number = route.params.eventId; 20 | const internetState: NetInfoState = useNetInfo(); 21 | const mapRef: MutableRefObject = useRef(null); 22 | const selectedEvent: Event = useSelector((state: any) => 23 | state.events.savedEvents.find((event: Event) => event.id === eventId) 24 | ); 25 | const initialRegionValue: Region = { 26 | latitude: selectedEvent.latitude, 27 | longitude: selectedEvent.longitude, 28 | latitudeDelta: 10, 29 | longitudeDelta: 10, 30 | }; 31 | let markerCoordinates: Coordinate = { 32 | latitude: selectedEvent.latitude ? selectedEvent.latitude : 0, 33 | longitude: selectedEvent.longitude ? selectedEvent.longitude : 0, 34 | }; 35 | 36 | useLayoutEffect(() => { 37 | navigation.setOptions({ 38 | headerLeft: () => ( 39 | 40 | navigation.goBack()} 48 | title="back" 49 | /> 50 | 51 | ), 52 | }); 53 | }, [navigation]); 54 | 55 | if (internetState.isConnected === false) { 56 | return ( 57 | 58 | 59 | Please turn on the Internet to use TamoTam. 60 | 61 | 62 | ); 63 | } 64 | 65 | const Map: () => JSX.Element = () => ( 66 | 67 | 76 | {markerCoordinates && ( 77 | 83 | )} 84 | 85 | 86 | ); 87 | 88 | return ( 89 | 90 | 91 | {selectedEvent.title} 92 | 93 | {selectedEvent.description} 94 | 95 | 96 | 🗓️{" "} 97 | { 98 | new Date(selectedEvent.date) instanceof Date 99 | ? new Date(selectedEvent.date).toLocaleDateString() 100 | : "No information" 101 | } 102 | 103 | 104 | 🕒{" "} 105 | { 106 | new Date(selectedEvent.date) instanceof Date 107 | ? new Date(selectedEvent.date).toLocaleTimeString([], { 108 | hour: "2-digit", 109 | minute: "2-digit", 110 | }) 111 | : "No information" 112 | } 113 | 114 | 122 | {!selectedEvent.latitude || !selectedEvent.longitude ? 123 | 124 | 125 | Problem with obtaining coordinates. 126 | 127 | : 128 | } 129 | 130 | 131 | ); 132 | } 133 | 134 | const styles = StyleSheet.create({ 135 | centered: { 136 | alignItems: "center", 137 | flex: 1, 138 | justifyContent: "center", 139 | }, 140 | container: { 141 | alignItems: "center", 142 | flex: 1, 143 | justifyContent: "center", 144 | marginHorizontal: 30, 145 | }, 146 | description: { 147 | marginBottom: 10, 148 | }, 149 | image: { 150 | alignItems: "center", 151 | borderColor: "#ccc", 152 | borderRadius: 10, 153 | borderWidth: 1, 154 | height: 200, 155 | justifyContent: "center", 156 | width: "100%", 157 | }, 158 | map: { 159 | height: Dimensions.get("window").height / 2, 160 | marginTop: 30, 161 | width: Dimensions.get("window").width, 162 | }, 163 | title: { 164 | fontSize: 20, 165 | fontWeight: "bold", 166 | textAlign: "center", 167 | }, 168 | }); 169 | -------------------------------------------------------------------------------- /constants/CustomMapStyles.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | CUSTOM_MAP_STYLES: [ 3 | { 4 | elementType: "geometry", 5 | stylers: [ 6 | { 7 | color: "#ebe3cd", 8 | }, 9 | ], 10 | }, 11 | { 12 | elementType: "labels.text.fill", 13 | stylers: [ 14 | { 15 | color: "#523735", 16 | }, 17 | ], 18 | }, 19 | { 20 | elementType: "labels.text.stroke", 21 | stylers: [ 22 | { 23 | color: "#f5f1e6", 24 | }, 25 | ], 26 | }, 27 | { 28 | featureType: "administrative", 29 | elementType: "geometry.stroke", 30 | stylers: [ 31 | { 32 | color: "#c9b2a6", 33 | }, 34 | ], 35 | }, 36 | { 37 | featureType: "administrative.land_parcel", 38 | elementType: "geometry.stroke", 39 | stylers: [ 40 | { 41 | color: "#dcd2be", 42 | }, 43 | ], 44 | }, 45 | { 46 | featureType: "administrative.land_parcel", 47 | elementType: "labels.text.fill", 48 | stylers: [ 49 | { 50 | color: "#ae9e90", 51 | }, 52 | ], 53 | }, 54 | { 55 | featureType: "landscape.natural", 56 | elementType: "geometry", 57 | stylers: [ 58 | { 59 | color: "#dfd2ae", 60 | }, 61 | ], 62 | }, 63 | { 64 | featureType: "poi", 65 | elementType: "geometry", 66 | stylers: [ 67 | { 68 | color: "#dfd2ae", 69 | }, 70 | ], 71 | }, 72 | { 73 | featureType: "poi", 74 | elementType: "labels.text.fill", 75 | stylers: [ 76 | { 77 | color: "#93817c", 78 | }, 79 | ], 80 | }, 81 | { 82 | featureType: "poi.business", 83 | elementType: "labels.icon", 84 | stylers: [ 85 | { 86 | visibility: "off", 87 | }, 88 | ], 89 | }, 90 | { 91 | featureType: "poi.business", 92 | elementType: "labels.text.fill", 93 | stylers: [ 94 | { 95 | visibility: "off", 96 | }, 97 | ], 98 | }, 99 | { 100 | featureType: "poi.medical", 101 | elementType: "geometry.fill", 102 | stylers: [ 103 | { 104 | color: "#feecf9", 105 | }, 106 | ], 107 | }, 108 | { 109 | featureType: "poi.medical", 110 | elementType: "labels.icon", 111 | stylers: [ 112 | { 113 | visibility: "off", 114 | }, 115 | ], 116 | }, 117 | { 118 | featureType: "poi.medical", 119 | elementType: "labels.text", 120 | stylers: [ 121 | { 122 | visibility: "off", 123 | }, 124 | ], 125 | }, 126 | { 127 | featureType: "poi.park", 128 | elementType: "geometry.fill", 129 | stylers: [ 130 | { 131 | color: "#a5b076", 132 | }, 133 | ], 134 | }, 135 | { 136 | featureType: "poi.park", 137 | elementType: "labels.text.fill", 138 | stylers: [ 139 | { 140 | color: "#447530", 141 | }, 142 | ], 143 | }, 144 | { 145 | featureType: "road", 146 | elementType: "geometry", 147 | stylers: [ 148 | { 149 | color: "#f5f1e6", 150 | }, 151 | ], 152 | }, 153 | { 154 | featureType: "road.arterial", 155 | elementType: "geometry", 156 | stylers: [ 157 | { 158 | color: "#fdfcf8", 159 | }, 160 | ], 161 | }, 162 | { 163 | featureType: "road.highway", 164 | elementType: "geometry", 165 | stylers: [ 166 | { 167 | color: "#f8c967", 168 | }, 169 | ], 170 | }, 171 | { 172 | featureType: "road.highway", 173 | elementType: "geometry.stroke", 174 | stylers: [ 175 | { 176 | color: "#e9bc62", 177 | }, 178 | ], 179 | }, 180 | { 181 | featureType: "road.highway.controlled_access", 182 | elementType: "geometry", 183 | stylers: [ 184 | { 185 | color: "#e98d58", 186 | }, 187 | ], 188 | }, 189 | { 190 | featureType: "road.highway.controlled_access", 191 | elementType: "geometry.stroke", 192 | stylers: [ 193 | { 194 | color: "#db8555", 195 | }, 196 | ], 197 | }, 198 | { 199 | featureType: "road.local", 200 | elementType: "labels.text.fill", 201 | stylers: [ 202 | { 203 | color: "#806b63", 204 | }, 205 | { 206 | visibility: "off", 207 | }, 208 | ], 209 | }, 210 | { 211 | featureType: "transit", 212 | elementType: "geometry.fill", 213 | stylers: [ 214 | { 215 | color: "#000000", 216 | }, 217 | { 218 | visibility: "off", 219 | }, 220 | ], 221 | }, 222 | { 223 | featureType: "transit.line", 224 | elementType: "geometry", 225 | stylers: [ 226 | { 227 | color: "#dfd2ae", 228 | }, 229 | ], 230 | }, 231 | { 232 | featureType: "transit.line", 233 | elementType: "labels.text.fill", 234 | stylers: [ 235 | { 236 | color: "#8f7d77", 237 | }, 238 | ], 239 | }, 240 | { 241 | featureType: "transit.line", 242 | elementType: "labels.text.stroke", 243 | stylers: [ 244 | { 245 | color: "#ebe3cd", 246 | }, 247 | ], 248 | }, 249 | { 250 | featureType: "transit.station", 251 | elementType: "geometry", 252 | stylers: [ 253 | { 254 | color: "#dfd2ae", 255 | }, 256 | ], 257 | }, 258 | { 259 | featureType: "water", 260 | elementType: "geometry.fill", 261 | stylers: [ 262 | { 263 | color: "#b9d3c2", 264 | }, 265 | ], 266 | }, 267 | { 268 | featureType: "water", 269 | elementType: "labels.text.fill", 270 | stylers: [ 271 | { 272 | color: "#92998d", 273 | }, 274 | ], 275 | }, 276 | ], 277 | }; 278 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 🤙 TamoTam. HangOut. Offline. 2 | 3 | [![TamoTam in Apple App Store](docs/AppleAppStoreButton.png)](https://apps.apple.com/pl/app/tamotam-hangout-offline/id1625649957) 4 | [![TamoTam in Google Play Store](docs/GooglePlayStoreButton.png)](https://play.google.com/store/apps/details?id=com.tamotam.application) 5 | 6 | ## Description 7 | 8 | Map with offline events happening around you. You don't need to log in or register. 9 | 10 | ### Go Online to be Offline. 11 | 12 | TamoTam aims to limit online time spent on digital applications in favor of offline social life outside the screen. 13 | 14 | The application aims to be minimalistic and exclude features such as: 15 | 16 | - Addictive Feed, 17 | - Destructive Notifications, 18 | - Brain-affecting Likes, 19 | - Comments, #StopHate, 20 | - Share, #StopFakeNews, 21 | - Other complicated Algorithms unconsciously affect your stay online. 22 | 23 | That's because we believe Social Media affects us mentally, such as Anxiety, Depression, Fear Of Missing Out (FOMO), Fear of Speaking Up, Isolation, and more. 24 | 25 | Look what's happening around you in Real. Offline. Social Life. 26 | Could you add your events (log in or registration required) to contribute to that idea? 27 | 28 | ### Note 29 | 30 | The application is in an early stage of development with a number of [improvements & issues](https://github.com/tamotam-com/tamotam-app/issues). 31 | It will take time before the application will be complete, performant and user-friendly, but in late 2023 we expect Early Beta. 32 | 33 | Thanks in advance for understanding while reviewing TamoTam. 34 | 35 | ## Developers 36 | 37 | That's an Open Source project; feel free to contribute. 38 | 39 | ### Frontend/JavaScript/Web Developer interested in React Native/JavaScript Mobile Development? 40 | 41 | The project is a combination of a personal education project to learn JavaScript Mobile Development using `React Native` and business idea to create a mobile app with offline-only events to limit online time. 42 | 43 | Therefore, even if that's just from business point of view super easy app for now, it contains several nice technical implementation, which helped me to understand the ecosystem on `React Native` and become better developer. 44 | 45 | ### Technological stack 46 | 47 | - `React/Native` + `TypeScript`; 48 | - `Redux`; 49 | - Different databases (`Firebase`, `SQLite`, `AsyncStorage`); 50 | - `AsyncStorage`, which is really comparable to `localStorage` we have on Web; 51 | - Caching data using `AsyncStorage`; 52 | - Making the Web code actually compiled & working with deployed app to the store; 53 | - Logging & Monitoring to `Firebase Analytics`/`Firebase Performance Monitoring`; 54 | - Reporting crashes to `Firebase Crashlytics`; 55 | - Handling environmental variables in `Android`, `iOS`, and `Web` - changes in native code were required; 56 | - Integration of 3rd party tools, like `Firebase`-* and `Google Maps`; 57 | - Usage, looks & feel of `Material Design` (`React Native Paper`) in a real application. 58 | 59 | ### Stay up to date 60 | 61 | Keep yourself up to date about TamoTam and me motivated by giving a **Star** :-) 62 | ![Star TamoTam on GitHub](docs/star.webm) 63 | 64 | ## Run it 65 | 66 | ### Android Simulator 67 | 68 | 1. Run on `Android Studio` 69 | 2. `yarn start` 70 | 3. `adb reverse tcp:8081 tcp:8081` 71 | 72 | Alternatively, `expo run:android --variant release`, for production version. 73 | 74 | #### Kill Android Simulator 75 | 76 | `adb -s emulator-5554 emu kill`, where `emulator-5554` is the emulator name. 77 | 78 | ### iOS Simulator 79 | 80 | 1. Build using `Xcode`, if the application isn't installed on the simulator 81 | 2. `yarn start` 82 | 3. `i` 83 | 84 | Alternatively, `expo run:ios --configuration Release`, for production version. 85 | 86 | ### Release 87 | 88 | 1. `eas build -p android` 89 | 2. `eas build -p ios` 90 | 91 | ## Architecture 92 | 93 | We're using `Redux`, but the easiest to understand the architecture is the image below with `Flux` architecture, which in fact is really similar. However, it's important to note we're using 1 store, like in `Redux` architecture. 94 | 95 | ![Data Flow Architecture image](docs/dataFlowArchitecture.png) 96 | *Image source: https://www.freecodecamp.org/news/an-introduction-to-the-flux-architectural-pattern-674ea74775c9/* 97 | 98 | External API's, like `Ticketmaster`, provide +/- 10k of external events. The `TamoTam`'s client is fetching also events added by the user, from `Firebase`. After all events will be fetched, those are being cached locally, using [AsyncStorage](https://github.com/react-native-async-storage/async-storage). After events are being cached locally on a device, the user can save their favourite events. Those saved events are also saved locally on a device. Those are saved using `SQLite`. In addition to that, users can add their own events, and those are saved in `Firebase`. 99 | 100 | ![Application Architecture image](docs/applicationArchitecture.svg) 101 | *Made using: https://app.diagrams.net* 102 | 103 | ## In media 104 | 105 | - [Codrops Newsletter](https://tympanus.net/codrops/collective/collective-736/) 106 | - [React Status Newsletter](https://react.statuscode.com/issues/310) 107 | - [Reddit r/reactnative](https://www.reddit.com/r/reactnative/comments/xzcxn8/react_native_typescript_app_with_firebase/) 108 | 109 | ## Research 110 | 111 | - [Brain Drain: The Mere Presence of One’s Own Smartphone Reduces Available Cognitive Capacity](https://www.journals.uchicago.edu/doi/full/10.1086/691462) 112 | - [No More FOMO: Limiting Social Media Decreases Loneliness and Depression](https://guilfordjournals.com/doi/10.1521/jscp.2018.37.10.751) 113 | - [Increases in Depression, Self‐Harm, and Suicide Among U.S. Adolescents After 2012 and Links to Technology Use: Possible Mechanisms](https://prcp.psychiatryonline.org/doi/full/10.1176/appi.prcp.20190015) 114 | - [A systematic review: the influence of social media on depression, anxiety and psychological distress in adolescents](https://www.tandfonline.com/doi/full/10.1080/02673843.2019.1590851) 115 | - [Cognitive Effects of Social Media Use: A Case of Older Adults](https://journals.sagepub.com/doi/full/10.1177/2056305118787203) 116 | - [Associations between smartphone use and mental health and well-being among young Swiss men](https://www.sciencedirect.com/science/article/pii/S002239562200588X) 117 | - [Excessive Smartphone Use Is Associated With Health Problems in Adolescents and Young Adults](https://www.frontiersin.org/articles/10.3389/fpsyt.2021.669042/full) 118 | 119 | ## Contact 120 | 121 | contact[at]tamotam[dot]com 122 | 123 | (demo) number of Android downloads badge: [https://playbadges.pavi2410.me/badge/full?id=com.tamotam.application](https://playbadges.pavi2410.me/badge/full?id=com.tamotam.application) 124 | -------------------------------------------------------------------------------- /helpers/sqlite_db.ts: -------------------------------------------------------------------------------- 1 | import * as SQLite from "expo-sqlite"; 2 | import analytics from "@react-native-firebase/analytics"; 3 | import crashlytics from "@react-native-firebase/crashlytics"; 4 | import { SQLError, SQLResultSet, SQLTransaction, WebSQLDatabase } from "expo-sqlite"; 5 | 6 | const sqlite_db: WebSQLDatabase = SQLite.openDatabase("savedEvents.db"); 7 | 8 | export const init: () => Promise = () => { 9 | const promise: Promise = new Promise((resolve: (value: unknown) => void, reject: (reason?: any) => void) => { 10 | sqlite_db.transaction((SQLiteTransaction: SQLTransaction) => { 11 | SQLiteTransaction.executeSql( 12 | `CREATE TABLE IF NOT EXISTS savedEvents (id INTEGER PRIMARY KEY NOT NULL, date REAL NOT NULL, description TEXT, firestoreDocumentId TEXT, imageUrl TEXT, isUserEvent INTEGER NOT NULL, latitude REAL NOT NULL, longitude REAL NOT NULL, title TEXT)`, 13 | [], 14 | (transaction: SQLTransaction, result: SQLResultSet) => { 15 | resolve(result); 16 | analytics().logEvent("custom_log", { 17 | description: "--- Analytics: helpers -> sqlite_db -> init -> (transaction, result), transaction: " + transaction, 18 | }); 19 | analytics().logEvent("custom_log", { 20 | description: "--- Analytics: helpers -> sqlite_db -> init -> (transaction, result), result: " + result, 21 | }); 22 | }, 23 | (transaction: SQLTransaction, error: SQLError | any): void | any => { 24 | reject(error); 25 | analytics().logEvent("custom_log", { 26 | description: "--- Analytics: helpers -> sqlite_db -> init -> (transaction, error), transaction: " + transaction, 27 | }); 28 | analytics().logEvent("custom_log", { 29 | description: "--- Analytics: helpers -> sqlite_db -> init -> (transaction, error), error: " + error, 30 | }); 31 | crashlytics().recordError(error); 32 | } 33 | ); 34 | }); 35 | }); 36 | 37 | return promise; 38 | }; 39 | 40 | export const deleteSavedEvent: (id: number | string) => Promise = ( 41 | id: number | string 42 | ) => { 43 | const promise: Promise = new Promise((resolve: (value: unknown) => void, reject: (reason?: any) => void) => { 44 | sqlite_db.transaction((SQLiteTransaction: SQLTransaction) => { 45 | SQLiteTransaction.executeSql(`DELETE FROM savedEvents WHERE id = ?;`, [id], 46 | (transaction: SQLTransaction, result: SQLResultSet) => { 47 | resolve(result); 48 | analytics().logEvent("custom_log", { 49 | description: "--- Analytics: helpers -> sqlite_db -> deleteSavedEvent -> (transaction, result), transaction: " + transaction, 50 | }); 51 | analytics().logEvent("custom_log", { 52 | description: "--- Analytics: helpers -> sqlite_db -> deleteSavedEvent -> (transaction, result), result: " + result, 53 | }); 54 | }, 55 | (transaction: SQLTransaction, error: SQLError | any): void | any => { 56 | reject(error); 57 | analytics().logEvent("custom_log", { 58 | description: "--- Analytics: helpers -> sqlite_db -> deleteSavedEvent -> (transaction, error), transaction: " + transaction, 59 | }); 60 | analytics().logEvent("custom_log", { 61 | description: "--- Analytics: helpers -> sqlite_db -> deleteSavedEvent -> (transaction, error), error: " + error, 62 | }); 63 | crashlytics().recordError(error); 64 | }) 65 | }) 66 | }); 67 | 68 | return promise; 69 | }; 70 | 71 | export const insertSavedEvent: 72 | (date: Date, description: string, firestoreDocumentId: string | undefined, imageUrl: string, isUserEvent: boolean, latitude: number, longitude: number, title: string) => Promise = ( 73 | date: Date, 74 | description: string, 75 | firestoreDocumentId: string | undefined, 76 | imageUrl: string, 77 | isUserEvent: boolean, 78 | latitude: number, 79 | longitude: number, 80 | title: string 81 | ) => { 82 | const promise: Promise = new Promise((resolve: (value: unknown) => void, reject: (reason?: any) => void) => { 83 | sqlite_db.transaction((SQLiteTransaction: SQLTransaction) => { 84 | SQLiteTransaction.executeSql( 85 | `INSERT INTO savedEvents (date, description, firestoreDocumentId, imageUrl, isUserEvent, latitude, longitude, title) VALUES (?, ?, ?, ?, ?, ?, ?, ?)`, 86 | [String(date), description, firestoreDocumentId ? firestoreDocumentId : null, imageUrl, Number(isUserEvent), latitude, longitude, title], 87 | (transaction: SQLTransaction, result: SQLResultSet) => { 88 | resolve(result); 89 | analytics().logEvent("custom_log", { 90 | description: "--- Analytics: helpers -> sqlite_db -> insertSavedEvent -> (transaction, result), transaction: " + transaction, 91 | }); 92 | analytics().logEvent("custom_log", { 93 | description: "--- Analytics: helpers -> sqlite_db -> insertSavedEvent -> (transaction, result), result: " + result, 94 | }); 95 | }, 96 | (transaction: SQLTransaction, error: SQLError | any): void | any => { 97 | reject(error); 98 | analytics().logEvent("custom_log", { 99 | description: "--- Analytics: helpers -> sqlite_db -> insertSavedEvent -> (transaction, error), transaction: " + transaction, 100 | }); 101 | analytics().logEvent("custom_log", { 102 | description: "--- Analytics: helpers -> sqlite_db -> insertSavedEvent -> (transaction, error), error: " + error, 103 | }); 104 | crashlytics().recordError(error); 105 | } 106 | ); 107 | }); 108 | }); 109 | 110 | return promise; 111 | }; 112 | 113 | export const fetchSavedEvents: () => Promise = () => { 114 | const promise: Promise = new Promise((resolve: (value: unknown) => void, reject: (reason?: any) => void) => { 115 | sqlite_db.transaction((SQLiteTransaction: SQLTransaction) => { 116 | SQLiteTransaction.executeSql( 117 | "SELECT * FROM savedEvents", 118 | [], 119 | (transaction: SQLTransaction, result: SQLResultSet) => { 120 | resolve(result); 121 | analytics().logEvent("custom_log", { 122 | description: "--- Analytics: helpers -> sqlite_db -> fetchSavedEvents -> (transaction, result), transaction: " + transaction, 123 | }); 124 | analytics().logEvent("custom_log", { 125 | description: "--- Analytics: helpers -> sqlite_db -> fetchSavedEvents -> (transaction, result), result: " + result, 126 | }); 127 | }, 128 | (transaction: SQLTransaction, error: SQLError | any): void | any => { 129 | reject(error); 130 | analytics().logEvent("custom_log", { 131 | description: "--- Analytics: helpers -> sqlite_db -> fetchSavedEvents -> (transaction, error), transaction: " + transaction, 132 | }); 133 | analytics().logEvent("custom_log", { 134 | description: "--- Analytics: helpers -> sqlite_db -> fetchSavedEvents -> (transaction, error), error: " + error, 135 | }); 136 | crashlytics().recordError(error); 137 | } 138 | ); 139 | }); 140 | }); 141 | 142 | return promise; 143 | }; 144 | -------------------------------------------------------------------------------- /android/gradlew: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # 4 | # Copyright © 2015-2021 the original authors. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # https://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | 19 | ############################################################################## 20 | # 21 | # Gradle start up script for POSIX generated by Gradle. 22 | # 23 | # Important for running: 24 | # 25 | # (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is 26 | # noncompliant, but you have some other compliant shell such as ksh or 27 | # bash, then to run this script, type that shell name before the whole 28 | # command line, like: 29 | # 30 | # ksh Gradle 31 | # 32 | # Busybox and similar reduced shells will NOT work, because this script 33 | # requires all of these POSIX shell features: 34 | # * functions; 35 | # * expansions «$var», «${var}», «${var:-default}», «${var+SET}», 36 | # «${var#prefix}», «${var%suffix}», and «$( cmd )»; 37 | # * compound commands having a testable exit status, especially «case»; 38 | # * various built-in commands including «command», «set», and «ulimit». 39 | # 40 | # Important for patching: 41 | # 42 | # (2) This script targets any POSIX shell, so it avoids extensions provided 43 | # by Bash, Ksh, etc; in particular arrays are avoided. 44 | # 45 | # The "traditional" practice of packing multiple parameters into a 46 | # space-separated string is a well documented source of bugs and security 47 | # problems, so this is (mostly) avoided, by progressively accumulating 48 | # options in "$@", and eventually passing that to Java. 49 | # 50 | # Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, 51 | # and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; 52 | # see the in-line comments for details. 53 | # 54 | # There are tweaks for specific operating systems such as AIX, CygWin, 55 | # Darwin, MinGW, and NonStop. 56 | # 57 | # (3) This script is generated from the Groovy template 58 | # https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt 59 | # within the Gradle project. 60 | # 61 | # You can find Gradle at https://github.com/gradle/gradle/. 62 | # 63 | ############################################################################## 64 | 65 | # Attempt to set APP_HOME 66 | 67 | # Resolve links: $0 may be a link 68 | app_path=$0 69 | 70 | # Need this for daisy-chained symlinks. 71 | while 72 | APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path 73 | [ -h "$app_path" ] 74 | do 75 | ls=$( ls -ld "$app_path" ) 76 | link=${ls#*' -> '} 77 | case $link in #( 78 | /*) app_path=$link ;; #( 79 | *) app_path=$APP_HOME$link ;; 80 | esac 81 | done 82 | 83 | APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit 84 | 85 | APP_NAME="Gradle" 86 | APP_BASE_NAME=${0##*/} 87 | 88 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 89 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' 90 | 91 | # Use the maximum available, or set MAX_FD != -1 to use that value. 92 | MAX_FD=maximum 93 | 94 | warn () { 95 | echo "$*" 96 | } >&2 97 | 98 | die () { 99 | echo 100 | echo "$*" 101 | echo 102 | exit 1 103 | } >&2 104 | 105 | # OS specific support (must be 'true' or 'false'). 106 | cygwin=false 107 | msys=false 108 | darwin=false 109 | nonstop=false 110 | case "$( uname )" in #( 111 | CYGWIN* ) cygwin=true ;; #( 112 | Darwin* ) darwin=true ;; #( 113 | MSYS* | MINGW* ) msys=true ;; #( 114 | NONSTOP* ) nonstop=true ;; 115 | esac 116 | 117 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 118 | 119 | 120 | # Determine the Java command to use to start the JVM. 121 | if [ -n "$JAVA_HOME" ] ; then 122 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 123 | # IBM's JDK on AIX uses strange locations for the executables 124 | JAVACMD=$JAVA_HOME/jre/sh/java 125 | else 126 | JAVACMD=$JAVA_HOME/bin/java 127 | fi 128 | if [ ! -x "$JAVACMD" ] ; then 129 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 130 | 131 | Please set the JAVA_HOME variable in your environment to match the 132 | location of your Java installation." 133 | fi 134 | else 135 | JAVACMD=java 136 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 137 | 138 | Please set the JAVA_HOME variable in your environment to match the 139 | location of your Java installation." 140 | fi 141 | 142 | # Increase the maximum file descriptors if we can. 143 | if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then 144 | case $MAX_FD in #( 145 | max*) 146 | MAX_FD=$( ulimit -H -n ) || 147 | warn "Could not query maximum file descriptor limit" 148 | esac 149 | case $MAX_FD in #( 150 | '' | soft) :;; #( 151 | *) 152 | ulimit -n "$MAX_FD" || 153 | warn "Could not set maximum file descriptor limit to $MAX_FD" 154 | esac 155 | fi 156 | 157 | # Collect all arguments for the java command, stacking in reverse order: 158 | # * args from the command line 159 | # * the main class name 160 | # * -classpath 161 | # * -D...appname settings 162 | # * --module-path (only if needed) 163 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. 164 | 165 | # For Cygwin or MSYS, switch paths to Windows format before running java 166 | if "$cygwin" || "$msys" ; then 167 | APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) 168 | CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) 169 | 170 | JAVACMD=$( cygpath --unix "$JAVACMD" ) 171 | 172 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 173 | for arg do 174 | if 175 | case $arg in #( 176 | -*) false ;; # don't mess with options #( 177 | /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath 178 | [ -e "$t" ] ;; #( 179 | *) false ;; 180 | esac 181 | then 182 | arg=$( cygpath --path --ignore --mixed "$arg" ) 183 | fi 184 | # Roll the args list around exactly as many times as the number of 185 | # args, so each arg winds up back in the position where it started, but 186 | # possibly modified. 187 | # 188 | # NB: a `for` loop captures its iteration list before it begins, so 189 | # changing the positional parameters here affects neither the number of 190 | # iterations, nor the values presented in `arg`. 191 | shift # remove old arg 192 | set -- "$@" "$arg" # push replacement arg 193 | done 194 | fi 195 | 196 | # Collect all arguments for the java command; 197 | # * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of 198 | # shell script including quotes and variable substitutions, so put them in 199 | # double quotes to make sure that they get re-expanded; and 200 | # * put everything else in single quotes, so that it's not re-expanded. 201 | 202 | set -- \ 203 | "-Dorg.gradle.appname=$APP_BASE_NAME" \ 204 | -classpath "$CLASSPATH" \ 205 | org.gradle.wrapper.GradleWrapperMain \ 206 | "$@" 207 | 208 | # Stop when "xargs" is not available. 209 | if ! command -v xargs >/dev/null 2>&1 210 | then 211 | die "xargs is not available" 212 | fi 213 | 214 | # Use "xargs" to parse quoted args. 215 | # 216 | # With -n1 it outputs one arg per line, with the quotes and backslashes removed. 217 | # 218 | # In Bash we could simply go: 219 | # 220 | # readarray ARGS < <( xargs -n1 <<<"$var" ) && 221 | # set -- "${ARGS[@]}" "$@" 222 | # 223 | # but POSIX shell has neither arrays nor command substitution, so instead we 224 | # post-process each arg (as a line of input to sed) to backslash-escape any 225 | # character that might be a shell metacharacter, then use eval to reverse 226 | # that process (while maintaining the separation between arguments), and wrap 227 | # the whole thing up as a single "set" statement. 228 | # 229 | # This will of course break if any of these variables contains a newline or 230 | # an unmatched quote. 231 | # 232 | 233 | eval "set -- $( 234 | printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | 235 | xargs -n1 | 236 | sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | 237 | tr '\n' ' ' 238 | )" '"$@"' 239 | 240 | exec "$JAVACMD" "$@" 241 | -------------------------------------------------------------------------------- /screens/SavedScreen.tsx: -------------------------------------------------------------------------------- 1 | import analytics from "@react-native-firebase/analytics"; 2 | import crashlytics from "@react-native-firebase/crashlytics"; 3 | import useColorScheme from "../hooks/useColorScheme"; 4 | import Colors from "../constants/Colors"; 5 | import EventItem from "../components/EventItem"; 6 | import MaterialHeaderButton from "../components/MaterialHeaderButton"; 7 | import React, { useCallback, useEffect, useLayoutEffect, useState, Dispatch } from "react"; 8 | import StyledText from "../components/StyledText"; 9 | import { deleteEvent } from "../store/actions/events"; 10 | import { fetchUsersSavedEvents } from "../store/actions/events"; 11 | import { useDispatch, useSelector } from "react-redux"; 12 | import { useNetInfo, NetInfoState } from "@react-native-community/netinfo"; 13 | import { ActivityIndicator, Alert, ColorSchemeName, FlatList, StyleSheet } from "react-native"; 14 | import { Button } from "react-native-paper"; 15 | import { Event } from "../interfaces/event"; 16 | import { HeaderButtons, Item } from "react-navigation-header-buttons"; 17 | import { View } from "../components/Themed"; 18 | 19 | export default function SavedScreen({ navigation, route }: any) { 20 | const colorScheme: ColorSchemeName = useColorScheme(); 21 | const dispatch: Dispatch = useDispatch>(); 22 | const internetState: NetInfoState = useNetInfo(); 23 | const savedEvents: Event[] = useSelector( 24 | (state: any) => state.events.savedEvents 25 | ); 26 | const [error, setError] = useState(new Error()); 27 | const [isLoading, setIsLoading] = useState(false); 28 | 29 | useEffect(() => { 30 | if (error.message !== "") { 31 | Alert.alert( 32 | "Unknown Error ❌", 33 | "Please report this error by sending an email to us at feedback@tamotam.com. It will help us 🙏\nError details: " + error.message + "\nDate: " + new Date(), 34 | [{ text: "Okay" }] 35 | ); 36 | analytics().logEvent("custom_log", { 37 | description: "--- Analytics: screens -> SavedScreen -> useEffect[error], error: " + error, 38 | }); 39 | crashlytics().recordError(error); 40 | } 41 | }, [error]); 42 | 43 | useLayoutEffect(() => { 44 | navigation.setOptions({ 45 | headerLeft: () => ( 46 | 47 | navigation.goBack()} 55 | title="back" 56 | /> 57 | 58 | ), 59 | }); 60 | }, [navigation]); 61 | 62 | const loadSavedEvents: () => Promise = useCallback(async () => { 63 | analytics().logEvent("custom_log", { 64 | description: "--- Analytics: screens -> SavedScreen -> loadSavedEvents", 65 | }); 66 | setError(new Error()); 67 | setIsLoading(true); 68 | 69 | try { 70 | analytics().logEvent("custom_log", { 71 | description: "--- Analytics: screens -> SavedScreen -> loadSavedEvents -> try", 72 | }); 73 | dispatch(fetchUsersSavedEvents()); 74 | } catch (error: unknown) { 75 | if (error instanceof Error) { 76 | Alert.alert( 77 | "Error ❌", 78 | "We couldn't load saved events, sorry.\nTry to reload TamoTam!", 79 | [{ text: "Okay" }] 80 | ); 81 | 82 | analytics().logEvent("custom_log", { 83 | description: "--- Analytics: screens -> SavedScreen -> loadSavedEvents -> catch, error: " + error, 84 | }); 85 | crashlytics().recordError(error); 86 | setError(new Error(error.message)); 87 | } 88 | } finally { 89 | analytics().logEvent("custom_log", { 90 | description: "--- Analytics: screens -> SavedScreen -> loadSavedEvents -> finally", 91 | }); 92 | setIsLoading(false); 93 | } 94 | }, [dispatch, setError, setIsLoading]); 95 | 96 | useEffect(() => { 97 | analytics().logEvent("custom_log", { 98 | description: "--- Analytics: screens -> SavedScreen -> useEffect[loadSavedEvents]", 99 | }); 100 | loadSavedEvents(); 101 | }, [loadSavedEvents]); 102 | 103 | if (isLoading) { 104 | return ( 105 | 106 | 110 | 111 | ); 112 | } 113 | 114 | if (internetState.isConnected === true && (savedEvents.length === 0 || !savedEvents)) { 115 | return ( 116 | 117 | 118 | No saved events found. Start adding some! 119 | 120 | 121 | ); 122 | } 123 | 124 | if (internetState.isConnected === false) { 125 | return ( 126 | 127 | 128 | Please turn on the Internet to use TamoTam. 129 | 130 | 131 | ); 132 | } 133 | 134 | const deleteHandler: (event: Event) => void = (event: Event) => { 135 | analytics().logEvent("custom_log", { 136 | description: "--- Analytics: screens -> SavedScreen -> deleteHandler", 137 | }); 138 | Alert.alert("⚠️ Delete saved event ⚠️", "Do you want to perform this irreversible deletion?", [ 139 | { text: "No", style: "default" }, 140 | { 141 | text: "Yes", 142 | style: "destructive", 143 | onPress: () => { 144 | setError(new Error()); 145 | setIsLoading(true); 146 | 147 | try { 148 | analytics().logEvent("custom_log", { 149 | description: "--- Analytics: screens -> SavedScreen -> deleteHandler -> try, event: " + event, 150 | }); 151 | dispatch(deleteEvent(event)); 152 | } catch (error: unknown) { 153 | if (error instanceof Error) { 154 | Alert.alert( 155 | "Error ❌", 156 | "TamoTam couldn't save this event.\nTry one more time!", 157 | [{ text: "Okay" }] 158 | ); 159 | 160 | analytics().logEvent("custom_log", { 161 | description: "--- Analytics: screens -> SavedScreen -> deleteHandler -> catch, error: " + error, 162 | }); 163 | crashlytics().recordError(error); 164 | setError(new Error(error.message)); 165 | } 166 | } finally { 167 | analytics().logEvent("custom_log", { 168 | description: "--- Analytics: screens -> SavedScreen -> deleteHandler -> finally", 169 | }); 170 | setIsLoading(false); 171 | } 172 | }, 173 | }, 174 | ]); 175 | }; 176 | 177 | return ( 178 | index.toString()} 181 | renderItem={(eventData: Event | any) => ( 182 | 193 | 212 | 230 | 245 | 246 | )} 247 | /> 248 | ); 249 | } 250 | 251 | const styles = StyleSheet.create({ 252 | centered: { 253 | alignItems: "center", 254 | flex: 1, 255 | justifyContent: "center", 256 | }, 257 | title: { 258 | fontSize: 20, 259 | fontWeight: "bold", 260 | textAlign: "center", 261 | }, 262 | }); 263 | -------------------------------------------------------------------------------- /android/app/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: "com.android.application" 2 | apply plugin: "com.facebook.react" 3 | 4 | import com.android.build.OutputFile 5 | apply from: project(':react-native-config').projectDir.getPath() + "/dotenv.gradle" 6 | 7 | 8 | def projectRoot = rootDir.getAbsoluteFile().getParentFile().getAbsolutePath() 9 | def expoDebuggableVariants = ['debug'] 10 | // Override `debuggableVariants` for expo-updates debugging 11 | if (System.getenv('EX_UPDATES_NATIVE_DEBUG') == "1") { 12 | react { 13 | expoDebuggableVariants = [] 14 | } 15 | } 16 | 17 | 18 | /** 19 | * This is the configuration block to customize your React Native Android app. 20 | * By default you don't need to apply any configuration, just uncomment the lines you need. 21 | */ 22 | react { 23 | entryFile = file(["node", "-e", "require('expo/scripts/resolveAppEntry')", projectRoot, "android", "absolute"].execute(null, rootDir).text.trim()) 24 | reactNativeDir = new File(["node", "--print", "require.resolve('react-native/package.json')"].execute(null, rootDir).text.trim()).getParentFile().getAbsoluteFile() 25 | hermesCommand = new File(["node", "--print", "require.resolve('react-native/package.json')"].execute(null, rootDir).text.trim()).getParentFile().getAbsolutePath() + "/sdks/hermesc/%OS-BIN%/hermesc" 26 | debuggableVariants = expoDebuggableVariants 27 | 28 | /* Folders */ 29 | // The root of your project, i.e. where "package.json" lives. Default is '..' 30 | // root = file("../") 31 | // The folder where the react-native NPM package is. Default is ../node_modules/react-native 32 | // reactNativeDir = file("../node_modules/react-native") 33 | // The folder where the react-native Codegen package is. Default is ../node_modules/react-native-codegen 34 | // codegenDir = file("../node_modules/react-native-codegen") 35 | // The cli.js file which is the React Native CLI entrypoint. Default is ../node_modules/react-native/cli.js 36 | // cliFile = file("../node_modules/react-native/cli.js") 37 | 38 | /* Variants */ 39 | // The list of variants to that are debuggable. For those we're going to 40 | // skip the bundling of the JS bundle and the assets. By default is just 'debug'. 41 | // If you add flavors like lite, prod, etc. you'll have to list your debuggableVariants. 42 | // debuggableVariants = ["liteDebug", "prodDebug"] 43 | 44 | /* Bundling */ 45 | // A list containing the node command and its flags. Default is just 'node'. 46 | // nodeExecutableAndArgs = ["node"] 47 | // 48 | // The command to run when bundling. By default is 'bundle' 49 | // bundleCommand = "ram-bundle" 50 | // 51 | // The path to the CLI configuration file. Default is empty. 52 | // bundleConfig = file(../rn-cli.config.js) 53 | // 54 | // The name of the generated asset file containing your JS bundle 55 | // bundleAssetName = "MyApplication.android.bundle" 56 | // 57 | // The entry file for bundle generation. Default is 'index.android.js' or 'index.js' 58 | // entryFile = file("../js/MyApplication.android.js") 59 | // 60 | // A list of extra flags to pass to the 'bundle' commands. 61 | // See https://github.com/react-native-community/cli/blob/main/docs/commands.md#bundle 62 | // extraPackagerArgs = [] 63 | 64 | /* Hermes Commands */ 65 | // The hermes compiler command to run. By default it is 'hermesc' 66 | // hermesCommand = "$rootDir/my-custom-hermesc/bin/hermesc" 67 | // 68 | // The list of flags to pass to the Hermes compiler. By default is "-O", "-output-source-map" 69 | // hermesFlags = ["-O", "-output-source-map"] 70 | } 71 | 72 | // Override `hermesEnabled` by `expo.jsEngine` 73 | ext { 74 | hermesEnabled = (findProperty('expo.jsEngine') ?: "hermes") == "hermes" 75 | } 76 | 77 | /** 78 | * Set this to true to create four separate APKs instead of one, 79 | * one for each native architecture. This is useful if you don't 80 | * use App Bundles (https://developer.android.com/guide/app-bundle/) 81 | * and want to have separate APKs to upload to the Play Store. 82 | */ 83 | def enableSeparateBuildPerCPUArchitecture = false 84 | 85 | /** 86 | * Set this to true to Run Proguard on Release builds to minify the Java bytecode. 87 | */ 88 | def enableProguardInReleaseBuilds = (findProperty('android.enableProguardInReleaseBuilds') ?: false).toBoolean() 89 | 90 | /** 91 | * The preferred build flavor of JavaScriptCore (JSC) 92 | * 93 | * For example, to use the international variant, you can use: 94 | * `def jscFlavor = 'org.webkit:android-jsc-intl:+'` 95 | * 96 | * The international variant includes ICU i18n library and necessary data 97 | * allowing to use e.g. `Date.toLocaleString` and `String.localeCompare` that 98 | * give correct results when using with locales other than en-US. Note that 99 | * this variant is about 6MiB larger per architecture than default. 100 | */ 101 | def jscFlavor = 'org.webkit:android-jsc:+' 102 | 103 | /** 104 | * Private function to get the list of Native Architectures you want to build. 105 | * This reads the value from reactNativeArchitectures in your gradle.properties 106 | * file and works together with the --active-arch-only flag of react-native run-android. 107 | */ 108 | def reactNativeArchitectures() { 109 | def value = project.getProperties().get("reactNativeArchitectures") 110 | return value ? value.split(",") : ["armeabi-v7a", "x86", "x86_64", "arm64-v8a"] 111 | } 112 | 113 | android { 114 | ndkVersion rootProject.ext.ndkVersion 115 | 116 | compileSdkVersion rootProject.ext.compileSdkVersion 117 | 118 | namespace "com.tamotam.application" 119 | defaultConfig { 120 | applicationId 'com.tamotam.application' 121 | minSdkVersion rootProject.ext.minSdkVersion 122 | resValue "string", "build_config_package", "com.tamotam.application" 123 | targetSdkVersion rootProject.ext.targetSdkVersion 124 | versionCode 95 125 | versionName "0.9.5" 126 | } 127 | 128 | splits { 129 | abi { 130 | reset() 131 | enable enableSeparateBuildPerCPUArchitecture 132 | universalApk false // If true, also generate a universal APK 133 | include (*reactNativeArchitectures()) 134 | } 135 | } 136 | signingConfigs { 137 | debug { 138 | storeFile file('debug.keystore') 139 | storePassword 'android' 140 | keyAlias 'androiddebugkey' 141 | keyPassword 'android' 142 | } 143 | } 144 | buildTypes { 145 | debug { 146 | signingConfig signingConfigs.debug 147 | } 148 | release { 149 | // Caution! In production, you need to generate your own keystore file. 150 | // see https://reactnative.dev/docs/signed-apk-android. 151 | signingConfig signingConfigs.debug 152 | minifyEnabled enableProguardInReleaseBuilds 153 | proguardFiles getDefaultProguardFile("proguard-android.txt"), "proguard-rules.pro" 154 | } 155 | } 156 | 157 | // applicationVariants are e.g. debug, release 158 | applicationVariants.all { variant -> 159 | variant.outputs.each { output -> 160 | // For each separate APK per architecture, set a unique version code as described here: 161 | // https://developer.android.com/studio/build/configure-apk-splits.html 162 | // Example: versionCode 1 will generate 1001 for armeabi-v7a, 1002 for x86, etc. 163 | def versionCodes = ["armeabi-v7a": 1, "x86": 2, "arm64-v8a": 3, "x86_64": 4] 164 | def abi = output.getFilter(OutputFile.ABI) 165 | if (abi != null) { // null for the universal-debug, universal-release variants 166 | output.versionCodeOverride = 167 | defaultConfig.versionCode * 1000 + versionCodes.get(abi) 168 | } 169 | 170 | } 171 | } 172 | } 173 | 174 | // Apply static values from `gradle.properties` to the `android.packagingOptions` 175 | // Accepts values in comma delimited lists, example: 176 | // android.packagingOptions.pickFirsts=/LICENSE,**/picasa.ini 177 | ["pickFirsts", "excludes", "merges", "doNotStrip"].each { prop -> 178 | // Split option: 'foo,bar' -> ['foo', 'bar'] 179 | def options = (findProperty("android.packagingOptions.$prop") ?: "").split(","); 180 | // Trim all elements in place. 181 | for (i in 0.. 0) { 186 | println "android.packagingOptions.$prop += $options ($options.length)" 187 | // Ex: android.packagingOptions.pickFirsts += '**/SCCS/**' 188 | options.each { 189 | android.packagingOptions[prop] += it 190 | } 191 | } 192 | } 193 | 194 | dependencies { 195 | // The version of react-native is set by the React Native Gradle Plugin 196 | implementation("com.facebook.react:react-android") 197 | 198 | def isGifEnabled = (findProperty('expo.gif.enabled') ?: "") == "true"; 199 | def isWebpEnabled = (findProperty('expo.webp.enabled') ?: "") == "true"; 200 | def isWebpAnimatedEnabled = (findProperty('expo.webp.animated') ?: "") == "true"; 201 | def frescoVersion = rootProject.ext.frescoVersion 202 | 203 | // If your app supports Android versions before Ice Cream Sandwich (API level 14) 204 | if (isGifEnabled || isWebpEnabled) { 205 | implementation("com.facebook.fresco:fresco:${frescoVersion}") 206 | implementation("com.facebook.fresco:imagepipeline-okhttp3:${frescoVersion}") 207 | } 208 | 209 | if (isGifEnabled) { 210 | // For animated gif support 211 | implementation("com.facebook.fresco:animated-gif:${frescoVersion}") 212 | } 213 | 214 | if (isWebpEnabled) { 215 | // For webp support 216 | implementation("com.facebook.fresco:webpsupport:${frescoVersion}") 217 | if (isWebpAnimatedEnabled) { 218 | // Animated webp support 219 | implementation("com.facebook.fresco:animated-webp:${frescoVersion}") 220 | } 221 | } 222 | 223 | implementation("androidx.swiperefreshlayout:swiperefreshlayout:1.0.0") 224 | 225 | debugImplementation("com.facebook.flipper:flipper:${FLIPPER_VERSION}") 226 | debugImplementation("com.facebook.flipper:flipper-network-plugin:${FLIPPER_VERSION}") { 227 | exclude group:'com.squareup.okhttp3', module:'okhttp' 228 | } 229 | debugImplementation("com.facebook.flipper:flipper-fresco-plugin:${FLIPPER_VERSION}") 230 | 231 | if (hermesEnabled.toBoolean()) { 232 | implementation("com.facebook.react:hermes-android") 233 | } else { 234 | implementation jscFlavor 235 | } 236 | } 237 | 238 | apply from: new File(["node", "--print", "require.resolve('@react-native-community/cli-platform-android/package.json')"].execute(null, rootDir).text.trim(), "../native_modules.gradle"); 239 | applyNativeModulesAppBuildGradle(project) 240 | 241 | apply plugin: 'com.google.gms.google-services' 242 | apply plugin: 'com.google.firebase.firebase-perf' 243 | apply plugin: 'com.google.firebase.crashlytics' 244 | -------------------------------------------------------------------------------- /store/actions/events.tsx: -------------------------------------------------------------------------------- 1 | import analytics from "@react-native-firebase/analytics"; 2 | import crashlytics from "@react-native-firebase/crashlytics"; 3 | import { deleteSavedEvent, fetchSavedEvents, insertSavedEvent } from "../../helpers/sqlite_db"; 4 | import { Alert } from "react-native"; 5 | import { Event } from "../../interfaces/event"; 6 | 7 | export const ADD_EVENT = "ADD_EVENT"; 8 | export const DELETE_EVENT = "DELETE_EVENT"; 9 | export const SAVE_EVENT = "SAVE_EVENT"; 10 | export const SET_EVENTS = "SET_EVENTS"; 11 | export const SET_SAVED_EVENTS = "SET_SAVED_EVENTS"; 12 | export const UPDATE_EVENT = "UPDATE_EVENT"; 13 | 14 | export const addEvent = (event: Event) => { 15 | return async ( 16 | dispatch: (arg0: { 17 | type: string; 18 | eventData: { 19 | id: number | string; 20 | date: Date; 21 | description: string; 22 | firestoreDocumentId: string; 23 | imageUrl: string; 24 | isUserEvent: boolean; 25 | latitude: number; 26 | longitude: number; 27 | title: string; 28 | }; 29 | }) => void 30 | ) => { 31 | try { 32 | dispatch({ 33 | type: ADD_EVENT, 34 | eventData: { 35 | id: event.id, 36 | date: event.date, 37 | description: event.description, 38 | firestoreDocumentId: event.firestoreDocumentId!, 39 | imageUrl: event.imageUrl, 40 | isUserEvent: event.isUserEvent, 41 | latitude: event.latitude, 42 | longitude: event.longitude, 43 | title: event.title, 44 | }, 45 | }); 46 | analytics().logEvent("custom_log", { 47 | description: "--- Analytics: store -> actions -> events -> addEvent -> try, event: " + event, 48 | }); 49 | } catch (error: unknown) { 50 | if (error instanceof Error) { 51 | Alert.alert( 52 | "Error ❌", 53 | "Adding an event has failed.", 54 | [{ text: "Okay" }] 55 | ); 56 | 57 | analytics().logEvent("custom_log", { 58 | description: "--- Analytics: store -> actions -> events -> addEvent -> catch, error: " + error, 59 | }); 60 | crashlytics().recordError(error); 61 | } 62 | } finally { 63 | Alert.alert( 64 | "Event added ✅", 65 | "You have successfully added this event.", 66 | [{ text: "Okay" }] 67 | ); 68 | analytics().logEvent("custom_log", { 69 | description: "--- Analytics: store -> actions -> events -> addEvent -> finally", 70 | }); 71 | } 72 | }; 73 | }; 74 | 75 | export const deleteEvent = (event: Event) => { 76 | return async ( 77 | dispatch: (arg0: { 78 | type: string; 79 | eventData: { 80 | id: number | string; 81 | date: Date; 82 | description: string; 83 | firestoreDocumentId?: string; 84 | imageUrl: string; 85 | isUserEvent: boolean; 86 | latitude: number; 87 | longitude: number; 88 | title: string; 89 | }; 90 | }) => void 91 | ) => { 92 | try { 93 | const dbResult: any = await deleteSavedEvent(event.id); 94 | 95 | dispatch({ 96 | type: DELETE_EVENT, 97 | eventData: { 98 | id: event.id, 99 | date: event.date, 100 | description: event.description, 101 | firestoreDocumentId: event.firestoreDocumentId, 102 | imageUrl: event.imageUrl, 103 | isUserEvent: event.isUserEvent, 104 | latitude: event.latitude, 105 | longitude: event.longitude, 106 | title: event.title, 107 | }, 108 | }); 109 | analytics().logEvent("custom_log", { 110 | description: "--- Analytics: store -> actions -> events -> deleteEvent -> try, dbResult: " + dbResult, 111 | }); 112 | analytics().logEvent("custom_log", { 113 | description: "--- Analytics: store -> actions -> events -> deleteEvent -> try, event: " + event, 114 | }); 115 | } catch (error: unknown) { 116 | if (error instanceof Error) { 117 | Alert.alert( 118 | "Error ❌", 119 | "Deleting a saved event has failed.", 120 | [{ text: "Okay" }] 121 | ); 122 | 123 | analytics().logEvent("custom_log", { 124 | description: "--- Analytics: store -> actions -> events -> deleteEvent -> catch, error: " + error, 125 | }); 126 | crashlytics().recordError(error); 127 | } 128 | } finally { 129 | Alert.alert( 130 | "Saved event deleted ✅", 131 | "You have successfully deleted this saved event, and it will no longer be visible.", 132 | [{ text: "Okay" }] 133 | ); 134 | analytics().logEvent("custom_log", { 135 | description: "--- Analytics: store -> actions -> events -> deleteEvent -> finally", 136 | }); 137 | } 138 | }; 139 | }; 140 | 141 | export const fetchUsersSavedEvents = () => { 142 | return async ( 143 | dispatch: (arg0: { savedEvents: Event[]; type: string }) => void 144 | ) => { 145 | try { 146 | const dbResult: any = await fetchSavedEvents(); 147 | 148 | dispatch({ 149 | savedEvents: dbResult.rows._array, 150 | type: SET_SAVED_EVENTS 151 | }); 152 | analytics().logEvent("custom_log", { 153 | description: "--- Analytics: store -> actions -> events -> fetchUsersSavedEvents -> try, dbResult: " + dbResult, 154 | }); 155 | } catch (error: unknown) { 156 | if (error instanceof Error) { 157 | Alert.alert( 158 | "Error ❌", 159 | "TamoTam couldn't fetch your saved events.", 160 | [{ text: "Okay" }] 161 | ); 162 | 163 | analytics().logEvent("custom_log", { 164 | description: "--- Analytics: store -> actions -> events -> fetchUsersSavedEvents -> catch, error: " + error, 165 | }); 166 | crashlytics().recordError(error); 167 | } 168 | } finally { 169 | Alert.alert( 170 | "Saved events loaded ✅", 171 | "These are stored on your local device as long as you won't explicitly clear the data or uninstall TamoTam.", 172 | [{ text: "Okay" }] 173 | ); 174 | analytics().logEvent("custom_log", { 175 | description: "--- Analytics: store -> actions -> events -> fetchUsersSavedEvents -> finally", 176 | }); 177 | } 178 | }; 179 | }; 180 | 181 | export const readItemFromStorage: (eventsFromAsyncStorage: Event[]) => void = (eventsFromAsyncStorage: Event[]) => { 182 | return async ( 183 | dispatch: (arg0: { events: Event[]; type: string }) => void 184 | ) => { 185 | try { 186 | dispatch({ 187 | type: SET_EVENTS, 188 | events: eventsFromAsyncStorage, 189 | }); 190 | analytics().logEvent("custom_log", { 191 | description: "--- Analytics: store -> actions -> events -> readItemFromStorage -> try, eventsFromAsyncStorage: " + eventsFromAsyncStorage, 192 | }); 193 | } catch (error: unknown) { 194 | if (error instanceof Error) { 195 | Alert.alert( 196 | "Error ❌", 197 | "Problem with loading events from a local device to avoid a big load during the next application launch.", 198 | [{ text: "Okay" }] 199 | ); 200 | 201 | analytics().logEvent("custom_log", { 202 | description: "--- Analytics: store -> actions -> events -> readItemFromStorage -> catch, error: " + error, 203 | }); 204 | crashlytics().recordError(error); 205 | } 206 | } finally { 207 | analytics().logEvent("custom_log", { 208 | description: "--- Analytics: store -> actions -> events -> readItemFromStorage -> finally", 209 | }); 210 | } 211 | } 212 | }; 213 | 214 | export const saveEvent = (event: Event) => { 215 | return async ( 216 | dispatch: (arg0: { 217 | type: string; 218 | eventData: { 219 | id: number | string; 220 | date: Date; 221 | description: string; 222 | firestoreDocumentId?: string; 223 | imageUrl: string; 224 | isUserEvent: boolean; 225 | latitude: number; 226 | longitude: number; 227 | title: string; 228 | }; 229 | }) => void 230 | ) => { 231 | try { 232 | const dbResult: any = await insertSavedEvent( 233 | event.date, 234 | event.description, 235 | event.firestoreDocumentId, 236 | event.imageUrl, 237 | event.isUserEvent, 238 | event.latitude, 239 | event.longitude, 240 | event.title 241 | ); 242 | 243 | dispatch({ 244 | type: SAVE_EVENT, 245 | eventData: { 246 | id: event.id, 247 | date: event.date, 248 | description: event.description, 249 | firestoreDocumentId: event.firestoreDocumentId, 250 | imageUrl: event.imageUrl, 251 | isUserEvent: event.isUserEvent, 252 | latitude: event.latitude, 253 | longitude: event.longitude, 254 | title: event.title, 255 | }, 256 | }); 257 | analytics().logEvent("custom_log", { 258 | description: "--- Analytics: store -> actions -> events -> saveEvent -> try, dbResult: " + dbResult, 259 | }); 260 | analytics().logEvent("custom_log", { 261 | description: "--- Analytics: store -> actions -> events -> saveEvent -> try, event: " + event, 262 | }); 263 | } catch (error: unknown) { 264 | if (error instanceof Error) { 265 | Alert.alert( 266 | "Error ❌", 267 | "TamoTam couldn't save your event.", 268 | [{ text: "Okay" }] 269 | ); 270 | 271 | analytics().logEvent("custom_log", { 272 | description: "--- Analytics: store -> actions -> events -> saveEvent -> catch, error: " + error, 273 | }); 274 | crashlytics().recordError(error); 275 | } 276 | } finally { 277 | Alert.alert( 278 | "Event saved ✅", 279 | "It will be visible in your saved events till you don't delete it.", 280 | [{ text: "Okay" }] 281 | ); 282 | analytics().logEvent("custom_log", { 283 | description: "--- Analytics: store -> actions -> events -> saveEvent -> finally", 284 | }); 285 | } 286 | }; 287 | }; 288 | 289 | export const updateEvent = (event: Event) => { 290 | return async ( 291 | dispatch: (arg0: { 292 | type: string; 293 | eventData: { 294 | id: number | string; 295 | date: Date; 296 | description: string; 297 | firestoreDocumentId: string; 298 | imageUrl: string; 299 | isUserEvent: boolean; 300 | latitude: number; 301 | longitude: number; 302 | title: string; 303 | }; 304 | }) => void 305 | ) => { 306 | try { 307 | dispatch({ 308 | type: UPDATE_EVENT, 309 | eventData: { 310 | id: event.id, 311 | date: event.date, 312 | description: event.description, 313 | firestoreDocumentId: event.firestoreDocumentId!, 314 | imageUrl: event.imageUrl, 315 | isUserEvent: event.isUserEvent, 316 | latitude: event.latitude, 317 | longitude: event.longitude, 318 | title: event.title, 319 | }, 320 | }); 321 | 322 | analytics().logEvent("custom_log", { 323 | description: "--- Analytics: store -> actions -> events -> updateEvent -> try, event: " + event, 324 | }); 325 | } 326 | catch (error: unknown) { 327 | if (error instanceof Error) { 328 | Alert.alert( 329 | "Error ❌", 330 | "TamoTam couldn't update this event.", 331 | [{ text: "Okay" }] 332 | ); 333 | 334 | analytics().logEvent("custom_log", { 335 | description: "--- Analytics: store -> actions -> events -> updateEvent -> catch, error: " + error, 336 | }); 337 | crashlytics().recordError(error); 338 | } 339 | } finally { 340 | Alert.alert( 341 | "Event updated ✅", 342 | "It will be visible in your saved events till you don't delete it.", 343 | [{ text: "Okay" }] 344 | ); 345 | } 346 | }; 347 | }; 348 | --------------------------------------------------------------------------------