├── .watchmanconfig ├── src ├── components │ ├── text │ │ ├── index.ts │ │ └── Text.tsx │ ├── layout │ │ ├── index.ts │ │ └── layout.tsx │ ├── criteria │ │ ├── index.ts │ │ └── Criteria.tsx │ ├── text-field │ │ ├── index.ts │ │ └── TextField.tsx │ ├── buttons │ │ ├── index.ts │ │ ├── FavoriteButton.tsx │ │ ├── RoundButton.tsx │ │ └── Button.tsx │ └── index.ts ├── screens │ ├── home-screen │ │ ├── index.ts │ │ ├── components │ │ │ ├── GridColumn.tsx │ │ │ ├── GridItem.tsx │ │ │ └── Spinner.tsx │ │ └── HomeScreen.tsx │ ├── account-screen │ │ ├── index.ts │ │ └── AccountScreen.tsx │ ├── browser-screen │ │ ├── index.ts │ │ └── BrowserScreen.tsx │ ├── details-screen │ │ ├── index.ts │ │ └── DetailsScreen.tsx │ ├── favorites-screen │ │ ├── index.ts │ │ └── FavoritesScreen.tsx │ └── index.ts ├── navigation │ ├── navigators │ │ ├── index.ts │ │ ├── HomeNavigator.tsx │ │ └── TabNavigator.tsx │ ├── index.ts │ ├── routes.ts │ └── config.ts ├── constants │ ├── web.ts │ ├── index.ts │ ├── metrics.ts │ ├── colors.ts │ └── firebase.ts ├── state │ ├── slices │ │ ├── index.ts │ │ ├── artworksSlice.ts │ │ ├── authSlice.ts │ │ └── favoritesSlice.ts │ ├── store │ │ ├── initialState.ts │ │ └── configureStore.ts │ └── types │ │ └── index.ts ├── api │ ├── index.ts │ ├── endpoints.ts │ ├── utils.ts │ ├── api.ts │ └── api.types.ts ├── model │ └── index.ts ├── hooks │ └── redux.ts ├── utils │ └── helpers.ts ├── App.tsx └── assets │ └── animations │ └── red-spinner.json ├── app.json ├── .eslintrc.js ├── android ├── app │ ├── debug.keystore │ ├── src │ │ ├── main │ │ │ ├── res │ │ │ │ ├── values │ │ │ │ │ ├── strings.xml │ │ │ │ │ ├── colors.xml │ │ │ │ │ ├── ic_launcher_background.xml │ │ │ │ │ └── styles.xml │ │ │ │ ├── drawable │ │ │ │ │ ├── splash_logo.png │ │ │ │ │ ├── bootsplash.xml │ │ │ │ │ └── rn_edit_text_material.xml │ │ │ │ ├── mipmap-hdpi │ │ │ │ │ └── ic_launcher_foreground.png │ │ │ │ ├── mipmap-mdpi │ │ │ │ │ └── ic_launcher_foreground.png │ │ │ │ ├── mipmap-xhdpi │ │ │ │ │ └── ic_launcher_foreground.png │ │ │ │ ├── mipmap-xxhdpi │ │ │ │ │ └── ic_launcher_foreground.png │ │ │ │ ├── mipmap-xxxhdpi │ │ │ │ │ └── ic_launcher_foreground.png │ │ │ │ └── mipmap-anydpi-v26 │ │ │ │ │ ├── ic_launcher.xml │ │ │ │ │ └── ic_launcher_round.xml │ │ │ ├── assets │ │ │ │ └── fonts │ │ │ │ │ └── Feather.ttf │ │ │ ├── ic_launcher-playstore.png │ │ │ ├── java │ │ │ │ └── com │ │ │ │ │ └── appart │ │ │ │ │ ├── MainActivity.java │ │ │ │ │ └── MainApplication.java │ │ │ └── AndroidManifest.xml │ │ └── debug │ │ │ ├── AndroidManifest.xml │ │ │ └── java │ │ │ └── com │ │ │ └── appart │ │ │ └── ReactNativeFlipper.java │ ├── proguard-rules.pro │ ├── build_defs.bzl │ ├── _BUCK │ └── build.gradle ├── gradle │ └── wrapper │ │ ├── gradle-wrapper.jar │ │ └── gradle-wrapper.properties ├── settings.gradle ├── gradle.properties ├── build.gradle ├── gradlew.bat └── gradlew ├── ios ├── appArt │ ├── Images.xcassets │ │ ├── Contents.json │ │ ├── Splash.imageset │ │ │ ├── iPhone_App_60_2x.png │ │ │ ├── iPhone_App_60_3x.png │ │ │ └── Contents.json │ │ └── AppIcon.appiconset │ │ │ ├── App_store_1024_1x.png │ │ │ ├── iPhone_App_60_2x.png │ │ │ ├── iPhone_App_60_3x.png │ │ │ ├── iPhone_Settings_29_2x.png │ │ │ ├── iPhone_Settings_29_3x.png │ │ │ ├── iPhone_Spotlight_40_2x.png │ │ │ ├── iPhone_Spotlight_40_3x.png │ │ │ ├── iPhone_Notifications_20_2x.png │ │ │ ├── iPhone_Notifications_20_3x.png │ │ │ └── Contents.json │ ├── AppDelegate.h │ ├── main.m │ ├── Info.plist │ ├── AppDelegate.m │ └── BootSplash.storyboard ├── appArt.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ └── IDEWorkspaceChecks.plist ├── appArtTests │ ├── Info.plist │ └── appArtTests.m ├── Podfile ├── GoogleService-Info.plist └── appArt.xcodeproj │ ├── xcshareddata │ └── xcschemes │ │ └── appArt.xcscheme │ └── project.pbxproj ├── .buckconfig ├── Gemfile ├── .prettierrc.js ├── reactotron-config.js ├── metro.config.js ├── index.js ├── README.md ├── .gitignore ├── babel.config.js ├── tsconfig.json ├── package.json └── Gemfile.lock /.watchmanconfig: -------------------------------------------------------------------------------- 1 | {} -------------------------------------------------------------------------------- /src/components/text/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./Text"; 2 | -------------------------------------------------------------------------------- /src/components/layout/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./layout"; 2 | -------------------------------------------------------------------------------- /src/components/criteria/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./Criteria"; 2 | -------------------------------------------------------------------------------- /src/components/text-field/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./TextField"; 2 | -------------------------------------------------------------------------------- /src/screens/home-screen/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./HomeScreen"; 2 | -------------------------------------------------------------------------------- /app.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "appArt", 3 | "displayName": "appArt" 4 | } -------------------------------------------------------------------------------- /src/navigation/navigators/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./HomeNavigator"; 2 | -------------------------------------------------------------------------------- /src/screens/account-screen/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./AccountScreen"; 2 | -------------------------------------------------------------------------------- /src/screens/browser-screen/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./BrowserScreen"; 2 | -------------------------------------------------------------------------------- /src/screens/details-screen/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./DetailsScreen"; 2 | -------------------------------------------------------------------------------- /src/screens/favorites-screen/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./FavoritesScreen"; 2 | -------------------------------------------------------------------------------- /src/constants/web.ts: -------------------------------------------------------------------------------- 1 | export const BASE_URL = "https://www.artic.edu/artworks/"; 2 | -------------------------------------------------------------------------------- /src/navigation/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./navigators"; 2 | export * from "./routes"; 3 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | extends: '@react-native-community', 4 | }; 5 | -------------------------------------------------------------------------------- /android/app/debug.keystore: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mxm87/appArt/HEAD/android/app/debug.keystore -------------------------------------------------------------------------------- /src/state/slices/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./artworksSlice"; 2 | export * from "./favoritesSlice"; 3 | -------------------------------------------------------------------------------- /src/api/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./api"; 2 | export * from "./api.types"; 3 | export * from "./utils"; 4 | -------------------------------------------------------------------------------- /src/constants/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./colors"; 2 | export * from "./metrics"; 3 | export * from "./web"; 4 | -------------------------------------------------------------------------------- /android/app/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | appArt 3 | 4 | -------------------------------------------------------------------------------- /ios/appArt/Images.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /android/gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mxm87/appArt/HEAD/android/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /android/app/src/main/assets/fonts/Feather.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mxm87/appArt/HEAD/android/app/src/main/assets/fonts/Feather.ttf -------------------------------------------------------------------------------- /android/app/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | #BF132B 3 | 4 | -------------------------------------------------------------------------------- /src/components/buttons/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./Button"; 2 | export * from "./RoundButton"; 3 | export * from "./FavoriteButton"; 4 | -------------------------------------------------------------------------------- /android/app/src/main/ic_launcher-playstore.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mxm87/appArt/HEAD/android/app/src/main/ic_launcher-playstore.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable/splash_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mxm87/appArt/HEAD/android/app/src/main/res/drawable/splash_logo.png -------------------------------------------------------------------------------- /.buckconfig: -------------------------------------------------------------------------------- 1 | 2 | [android] 3 | target = Google Inc.:Google APIs:23 4 | 5 | [maven_repositories] 6 | central = https://repo1.maven.org/maven2 7 | -------------------------------------------------------------------------------- /src/api/endpoints.ts: -------------------------------------------------------------------------------- 1 | const ENDPOINTS = { 2 | ARTWORKS: "api/v1/artworks", 3 | SEARCH: "api/v1/artworks/search", 4 | }; 5 | 6 | export default ENDPOINTS; 7 | -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mxm87/appArt/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/mxm87/appArt/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/mxm87/appArt/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/mxm87/appArt/HEAD/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /ios/appArt/Images.xcassets/Splash.imageset/iPhone_App_60_2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mxm87/appArt/HEAD/ios/appArt/Images.xcassets/Splash.imageset/iPhone_App_60_2x.png -------------------------------------------------------------------------------- /ios/appArt/Images.xcassets/Splash.imageset/iPhone_App_60_3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mxm87/appArt/HEAD/ios/appArt/Images.xcassets/Splash.imageset/iPhone_App_60_3x.png -------------------------------------------------------------------------------- /src/components/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./buttons"; 2 | export * from "./layout"; 3 | export * from "./text"; 4 | export * from "./text-field"; 5 | export * from "./criteria"; 6 | -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mxm87/appArt/HEAD/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /ios/appArt/Images.xcassets/AppIcon.appiconset/App_store_1024_1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mxm87/appArt/HEAD/ios/appArt/Images.xcassets/AppIcon.appiconset/App_store_1024_1x.png -------------------------------------------------------------------------------- /ios/appArt/Images.xcassets/AppIcon.appiconset/iPhone_App_60_2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mxm87/appArt/HEAD/ios/appArt/Images.xcassets/AppIcon.appiconset/iPhone_App_60_2x.png -------------------------------------------------------------------------------- /ios/appArt/Images.xcassets/AppIcon.appiconset/iPhone_App_60_3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mxm87/appArt/HEAD/ios/appArt/Images.xcassets/AppIcon.appiconset/iPhone_App_60_3x.png -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | # You may use http://rbenv.org/ or https://rvm.io/ to install and use this version 3 | ruby '2.7.4' 4 | gem 'cocoapods', '~> 1.11', '>= 1.11.2' 5 | -------------------------------------------------------------------------------- /android/app/src/main/res/values/ic_launcher_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #BF142C 4 | -------------------------------------------------------------------------------- /ios/appArt/Images.xcassets/AppIcon.appiconset/iPhone_Settings_29_2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mxm87/appArt/HEAD/ios/appArt/Images.xcassets/AppIcon.appiconset/iPhone_Settings_29_2x.png -------------------------------------------------------------------------------- /ios/appArt/Images.xcassets/AppIcon.appiconset/iPhone_Settings_29_3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mxm87/appArt/HEAD/ios/appArt/Images.xcassets/AppIcon.appiconset/iPhone_Settings_29_3x.png -------------------------------------------------------------------------------- /ios/appArt/Images.xcassets/AppIcon.appiconset/iPhone_Spotlight_40_2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mxm87/appArt/HEAD/ios/appArt/Images.xcassets/AppIcon.appiconset/iPhone_Spotlight_40_2x.png -------------------------------------------------------------------------------- /ios/appArt/Images.xcassets/AppIcon.appiconset/iPhone_Spotlight_40_3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mxm87/appArt/HEAD/ios/appArt/Images.xcassets/AppIcon.appiconset/iPhone_Spotlight_40_3x.png -------------------------------------------------------------------------------- /ios/appArt/Images.xcassets/AppIcon.appiconset/iPhone_Notifications_20_2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mxm87/appArt/HEAD/ios/appArt/Images.xcassets/AppIcon.appiconset/iPhone_Notifications_20_2x.png -------------------------------------------------------------------------------- /ios/appArt/Images.xcassets/AppIcon.appiconset/iPhone_Notifications_20_3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mxm87/appArt/HEAD/ios/appArt/Images.xcassets/AppIcon.appiconset/iPhone_Notifications_20_3x.png -------------------------------------------------------------------------------- /.prettierrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | bracketSpacing: true, 3 | jsxBracketSameLine: true, 4 | singleQuote: false, 5 | trailingComma: "all", 6 | arrowParens: "avoid", 7 | tabWidth: 4, 8 | }; 9 | -------------------------------------------------------------------------------- /src/screens/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./details-screen"; 2 | export * from "./favorites-screen"; 3 | export * from "./home-screen"; 4 | export * from "./browser-screen"; 5 | export * from "./account-screen"; 6 | -------------------------------------------------------------------------------- /android/settings.gradle: -------------------------------------------------------------------------------- 1 | rootProject.name = 'appArt' 2 | apply from: file("../node_modules/@react-native-community/cli-platform-android/native_modules.gradle"); applyNativeModulesSettingsGradle(settings) 3 | include ':app' 4 | -------------------------------------------------------------------------------- /ios/appArt/AppDelegate.h: -------------------------------------------------------------------------------- 1 | #import 2 | #import 3 | 4 | @interface AppDelegate : UIResponder 5 | 6 | @property (nonatomic, strong) UIWindow *window; 7 | 8 | @end 9 | -------------------------------------------------------------------------------- /ios/appArt/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 | -------------------------------------------------------------------------------- /android/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-7.2-all.zip 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | -------------------------------------------------------------------------------- /ios/appArt.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /src/model/index.ts: -------------------------------------------------------------------------------- 1 | type ArtItem = { 2 | id: number; 3 | title: string; 4 | artist_title: string; 5 | image_id: string; 6 | }; 7 | 8 | export type FavoriteItem = ArtItem & { 9 | timestamp: number; 10 | }; 11 | 12 | export type GridItem = ArtItem & { 13 | tileIndex: number; 14 | }; 15 | -------------------------------------------------------------------------------- /ios/appArt.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /reactotron-config.js: -------------------------------------------------------------------------------- 1 | import Reactotron from "reactotron-react-native"; 2 | import { NativeModules } from "react-native"; 3 | 4 | const host = NativeModules.SourceCode.scriptURL.split("://")[1].split(":")[0]; 5 | const reactotron = Reactotron.configure({ host }).useReactNative().connect(); 6 | 7 | export default reactotron; 8 | -------------------------------------------------------------------------------- /src/hooks/redux.ts: -------------------------------------------------------------------------------- 1 | import { TypedUseSelectorHook, useDispatch, useSelector } from "react-redux"; 2 | import type { RootState, AppDispatch } from "@state/store/configureStore"; 3 | 4 | export const useAppDispatch = () => useDispatch(); 5 | export const useAppSelector: TypedUseSelectorHook = useSelector; 6 | -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/state/store/initialState.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | auth: { 3 | user: null, 4 | loading: false, 5 | error: null, 6 | }, 7 | artworks: { 8 | data: [], 9 | loading: false, 10 | }, 11 | favorites: { 12 | ids: [], 13 | entities: {}, 14 | }, 15 | }; 16 | -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/navigation/routes.ts: -------------------------------------------------------------------------------- 1 | const ROUTES = { 2 | TAB_NAV: "TabNav", 3 | HOME: "Home", 4 | HOME_WRAPPER: "HomeWrapper", 5 | FAVORITES: "Favorites", 6 | ACCOUNT: "Account", 7 | DETAILS: "Details", 8 | BROWSER: "Browser", 9 | }; 10 | 11 | export default ROUTES; 12 | -------------------------------------------------------------------------------- /src/constants/metrics.ts: -------------------------------------------------------------------------------- 1 | import { Dimensions, Platform } from "react-native"; 2 | 3 | export const IS_IOS = Platform.OS === "ios"; 4 | 5 | export const { width: S_WIDTH, height: S_HEIGT } = Dimensions.get("window"); 6 | 7 | export const V_SCALE = S_HEIGT / 844; 8 | 9 | export const TILE_HEIGHT = { 10 | SMALL: 180, 11 | LARGE: 250, 12 | }; 13 | -------------------------------------------------------------------------------- /metro.config.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Metro configuration for React Native 3 | * https://github.com/facebook/react-native 4 | * 5 | * @format 6 | */ 7 | 8 | module.exports = { 9 | transformer: { 10 | getTransformOptions: async () => ({ 11 | transform: { 12 | experimentalImportSupport: false, 13 | inlineRequires: true, 14 | }, 15 | }), 16 | }, 17 | }; 18 | -------------------------------------------------------------------------------- /android/app/src/main/res/drawable/bootsplash.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /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 | # Add any project specific keep options here: 11 | -------------------------------------------------------------------------------- /ios/appArt/Images.xcassets/Splash.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "filename" : "iPhone_App_60_2x.png", 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "filename" : "iPhone_App_60_3x.png", 14 | "idiom" : "universal", 15 | "scale" : "3x" 16 | } 17 | ], 18 | "info" : { 19 | "author" : "xcode", 20 | "version" : 1 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/utils/helpers.ts: -------------------------------------------------------------------------------- 1 | import { Alert } from "react-native"; 2 | import { GridItem, FavoriteItem } from "@model/index"; 3 | 4 | type AlertProps = { 5 | title: string; 6 | description: string; 7 | }; 8 | 9 | export const showAlert = (alert: AlertProps) => { 10 | Alert.alert(alert.title, alert.description, [ 11 | { text: "OK", onPress: () => {} }, 12 | ]); 13 | }; 14 | 15 | export const getFavoriteItem = (item: GridItem): FavoriteItem => { 16 | const { tileIndex, ...rest } = item; 17 | return { ...rest, timestamp: Date.now() }; 18 | }; 19 | -------------------------------------------------------------------------------- /android/app/src/debug/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | import "react-native-gesture-handler"; 2 | import { LogBox } from "react-native"; 3 | 4 | if (__DEV__) { 5 | import("./reactotron-config").then(() => 6 | console.log("Reactotron Configured") 7 | ); 8 | } 9 | 10 | LogBox.ignoreLogs([ 11 | "[react-native-gesture-handler] Seems like you're using an old API with gesture components, check out new Gestures system!", 12 | ]); 13 | 14 | import { AppRegistry } from "react-native"; 15 | import App from "./src/App"; 16 | import { name as appName } from "./app.json"; 17 | 18 | AppRegistry.registerComponent(appName, () => App); 19 | -------------------------------------------------------------------------------- /android/app/build_defs.bzl: -------------------------------------------------------------------------------- 1 | """Helper definitions to glob .aar and .jar targets""" 2 | 3 | def create_aar_targets(aarfiles): 4 | for aarfile in aarfiles: 5 | name = "aars__" + aarfile[aarfile.rindex("/") + 1:aarfile.rindex(".aar")] 6 | lib_deps.append(":" + name) 7 | android_prebuilt_aar( 8 | name = name, 9 | aar = aarfile, 10 | ) 11 | 12 | def create_jar_targets(jarfiles): 13 | for jarfile in jarfiles: 14 | name = "jars__" + jarfile[jarfile.rindex("/") + 1:jarfile.rindex(".jar")] 15 | lib_deps.append(":" + name) 16 | prebuilt_jar( 17 | name = name, 18 | binary_jar = jarfile, 19 | ) 20 | -------------------------------------------------------------------------------- /android/app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 11 | 12 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /ios/appArtTests/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | BNDL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | 1 23 | 24 | 25 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | **[22.06.22 Update]** 2 | New features: 3 | 1. Form handling: 4 | - Login form with Formik 5 | - Field validation with yup 6 | - Criteria for password field 7 | 2. Firebase (Auth / Firestore) with Redux integration: 8 | - Sign up / sign in with email 9 | - Sync your data to the cloud 10 | 11 | --- 12 | 13 | This app is based on The Art Institute of Chicago API. You can fetch 8 random artworks, save favorite ones, get full description via in-app browser. 14 | 15 | https://user-images.githubusercontent.com/47810008/175026480-97b53055-155e-49a4-bcb1-3d0b892b1e30.mp4 16 | 17 | What you will find here: 18 | - State management w/ API calls and persistence (Redux Toolkit, AsyncThunk, Redux Persist) 19 | - UI (components w/ pre-defined styles, shared transitions, pull to refresh spinner) 20 | - In-App browser (WebView) 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /src/screens/home-screen/components/GridColumn.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { View } from "react-native"; 3 | import { ArtItem } from "@model/index"; 4 | import GridItem from "./GridItem"; 5 | 6 | type GridColumnPros = { 7 | position: "left" | "right"; 8 | data: ArtItem[]; 9 | }; 10 | 11 | const GridColumn = ({ position, data }: GridColumnPros) => { 12 | const smallTileIndex = position === "left" ? 0 : data.length - 1; 13 | 14 | return ( 15 | 16 | {data.map((item, index) => { 17 | return ( 18 | 22 | ); 23 | })} 24 | 25 | ); 26 | }; 27 | 28 | export default GridColumn; 29 | -------------------------------------------------------------------------------- /src/components/layout/layout.tsx: -------------------------------------------------------------------------------- 1 | import React, { ReactNode } from "react"; 2 | import { View, ViewStyle } from "react-native"; 3 | 4 | type LProps = { 5 | children?: ReactNode; 6 | style?: ViewStyle; 7 | }; 8 | 9 | export const Container = ({ children, style, ...props }: LProps) => ( 10 | 17 | {children} 18 | 19 | ); 20 | 21 | export const Flex = ({ children, style, ...props }: LProps) => ( 22 | 23 | {children} 24 | 25 | ); 26 | 27 | export const HStack = ({ children, style, ...props }: LProps) => ( 28 | 29 | {children} 30 | 31 | ); 32 | -------------------------------------------------------------------------------- /ios/Podfile: -------------------------------------------------------------------------------- 1 | require_relative '../node_modules/react-native/scripts/react_native_pods' 2 | require_relative '../node_modules/@react-native-community/cli-platform-ios/native_modules' 3 | 4 | platform :ios, '11.0' 5 | 6 | target 'appArt' do 7 | config = use_native_modules! 8 | 9 | use_react_native!( 10 | :path => config[:reactNativePath], 11 | # to enable hermes on iOS, change `false` to `true` and then install pods 12 | :hermes_enabled => false 13 | ) 14 | 15 | target 'appArtTests' do 16 | inherit! :complete 17 | # Pods for testing 18 | end 19 | 20 | # Enables Flipper. 21 | # 22 | # Note that if you have use_frameworks! enabled, Flipper will not work and 23 | # you should disable the next line. 24 | use_flipper!() 25 | 26 | post_install do |installer| 27 | react_native_post_install(installer) 28 | __apply_Xcode_12_5_M1_post_install_workaround(installer) 29 | end 30 | end -------------------------------------------------------------------------------- /src/api/utils.ts: -------------------------------------------------------------------------------- 1 | export const random = (min: number, max: number) => 2 | Math.floor(Math.random() * (max - min)) + min; 3 | 4 | export const getImageURI = (image_id: string, small: boolean = false) => { 5 | const size = small ? 200 : 843; 6 | return `https://www.artic.edu/iiif/2/${image_id}/full/${size},/0/default.jpg`; 7 | }; 8 | 9 | export const getSearchParams = () => { 10 | return { 11 | q: "paintings", 12 | limit: 8, 13 | page: random(1, 100), 14 | query: { 15 | bool: { 16 | must: [ 17 | { term: { is_public_domain: true } }, 18 | { 19 | script: { 20 | script: "doc['thumbnail.height'].value > doc['thumbnail.width'].value", 21 | }, 22 | }, 23 | ], 24 | }, 25 | }, 26 | }; 27 | }; 28 | -------------------------------------------------------------------------------- /src/components/buttons/FavoriteButton.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { TouchableOpacity, StyleSheet } from "react-native"; 3 | import { COLORS } from "@constants/index"; 4 | import Icon from "react-native-vector-icons/Feather"; 5 | 6 | type FavoriteButtonProps = { 7 | onPress: () => void; 8 | active: boolean; 9 | }; 10 | 11 | export const FavoriteButton = ({ onPress, active }: FavoriteButtonProps) => { 12 | return ( 13 | 14 | 19 | 20 | ); 21 | }; 22 | 23 | const styles = StyleSheet.create({ 24 | button: { 25 | width: 48, 26 | height: 48, 27 | alignItems: "center", 28 | justifyContent: "center", 29 | }, 30 | }); 31 | -------------------------------------------------------------------------------- /android/app/src/main/java/com/appart/MainActivity.java: -------------------------------------------------------------------------------- 1 | package com.appart; 2 | 3 | // [react-native-bootsplash] - start 4 | import android.os.Bundle; 5 | // [react-native-bootsplash] - end 6 | 7 | import com.facebook.react.ReactActivity; 8 | 9 | // [react-native-bootsplash] - start 10 | import com.zoontek.rnbootsplash.RNBootSplash; 11 | // [react-native-bootsplash] - end 12 | 13 | public class MainActivity extends ReactActivity { 14 | 15 | /** 16 | * Returns the name of the main component registered from JavaScript. This is used to schedule 17 | * rendering of the component. 18 | */ 19 | @Override 20 | protected String getMainComponentName() { 21 | return "appArt"; 22 | } 23 | 24 | // [react-native-bootsplash] - start 25 | @Override 26 | protected void onCreate(Bundle savedInstanceState) { 27 | super.onCreate(null); 28 | RNBootSplash.init(R.drawable.bootsplash, MainActivity.this); 29 | } 30 | // [react-native-bootsplash] - end 31 | } 32 | -------------------------------------------------------------------------------- /src/constants/colors.ts: -------------------------------------------------------------------------------- 1 | const palette = { 2 | translucentWhite: "rgba(255,255,255,0.8)", 3 | moreTranslucentWhite: "rgba(255,255,255,0.5)", 4 | translucentBlack: "rgba(0,0,0,0.8)", 5 | white: "#FFFFFF", 6 | black: "#000000", 7 | gray: "#F2F2F2", 8 | alabaster: "#F9F9F9", 9 | doveGray: "#707070", 10 | emperorGray: "#505050", 11 | alto: "#DDDDDD", 12 | shiraz: "#C20A2A", 13 | amaranth: "#EA2B4D", 14 | ao: "#008038", 15 | }; 16 | 17 | export const COLORS = { 18 | DEFAULT: palette.gray, 19 | ACTIVE: palette.shiraz, 20 | ARTIST_FONT: palette.doveGray, 21 | BG: palette.translucentWhite, 22 | ART_BG: palette.alto, 23 | BUTTON: palette.translucentBlack, 24 | BUTTON_WRAPPER: palette.moreTranslucentWhite, 25 | INPUT: palette.translucentBlack, 26 | ICON: palette.emperorGray, 27 | ICON_BG: palette.alabaster, 28 | CRITERIA: palette.ao, 29 | WHITE: palette.white, 30 | BLACK: palette.black, 31 | }; 32 | -------------------------------------------------------------------------------- /src/screens/browser-screen/BrowserScreen.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { View, StyleSheet } from "react-native"; 3 | import { WebView } from "react-native-webview"; 4 | import { Flex, RoundButton } from "@components/index"; 5 | 6 | export const BrowserScreen = ({ navigation, route }) => { 7 | const onBackButtonPress = () => { 8 | navigation.goBack(); 9 | }; 10 | 11 | return ( 12 | 13 | 19 | 20 | 21 | 22 | 23 | ); 24 | }; 25 | 26 | const styles = StyleSheet.create({ 27 | closeButtonWrapper: { 28 | position: "absolute", 29 | top: 48, 30 | left: 16, 31 | }, 32 | }); 33 | -------------------------------------------------------------------------------- /src/navigation/config.ts: -------------------------------------------------------------------------------- 1 | import { SharedElementsComponentConfig } from "react-navigation-shared-element"; 2 | import { StackCardStyleInterpolator } from "@react-navigation/stack"; 3 | import ROUTES from "./routes"; 4 | 5 | export const transitionInterpolation: StackCardStyleInterpolator = ({ 6 | current, 7 | }) => ({ 8 | cardStyle: { 9 | opacity: current.progress.interpolate({ 10 | inputRange: [0, 1], 11 | outputRange: [0, 1], 12 | extrapolate: "clamp", 13 | }), 14 | }, 15 | }); 16 | 17 | export const sharedElementsConfig: SharedElementsComponentConfig = ( 18 | route, 19 | otherRoute, 20 | showing 21 | ) => { 22 | return [ 23 | otherRoute.name === ROUTES.HOME && { 24 | id: `artwork.${ 25 | showing 26 | ? route.params.initialScrollIndex 27 | : otherRoute.params.currentScrollIndex 28 | }`, 29 | animation: "move", 30 | }, 31 | ]; 32 | }; 33 | -------------------------------------------------------------------------------- /src/state/types/index.ts: -------------------------------------------------------------------------------- 1 | import { Api } from "@api/api"; 2 | import { AppDispatch, RootState } from "@state/store/configureStore"; 3 | import { FirebaseFirestoreTypes } from "@react-native-firebase/firestore"; 4 | 5 | export type TThunkAPI = { 6 | dispatch: AppDispatch; 7 | getState: () => RootState; 8 | extra: { 9 | apiService: Api; 10 | dbRef: FirebaseFirestoreTypes.CollectionReference; 11 | }; 12 | rejectValue: string; 13 | }; 14 | 15 | export type User = { 16 | displayName: any; 17 | email: string; 18 | emailVerified: boolean; 19 | isAnonymous: boolean; 20 | metadata: { 21 | creationTime: number; 22 | lastSignInTime: number; 23 | }; 24 | phoneNumber: any; 25 | photoURL: any; 26 | providerData: Array<{ 27 | email: string; 28 | providerId: string; 29 | uid: string; 30 | }>; 31 | providerId: string; 32 | refreshToken: string; 33 | tenantId: any; 34 | uid: string; 35 | }; 36 | -------------------------------------------------------------------------------- /.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 | 24 | # Android/IntelliJ 25 | # 26 | build/ 27 | .idea 28 | .gradle 29 | local.properties 30 | *.iml 31 | *.hprof 32 | 33 | # node.js 34 | # 35 | node_modules/ 36 | npm-debug.log 37 | yarn-error.log 38 | 39 | # BUCK 40 | buck-out/ 41 | \.buckd/ 42 | *.keystore 43 | !debug.keystore 44 | 45 | # fastlane 46 | # 47 | # It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the 48 | # screenshots whenever they are needed. 49 | # For more information about the recommended setup visit: 50 | # https://docs.fastlane.tools/best-practices/source-control/ 51 | 52 | */fastlane/report.xml 53 | */fastlane/Preview.html 54 | */fastlane/screenshots 55 | 56 | # Bundle artifact 57 | *.jsbundle 58 | 59 | # CocoaPods 60 | /ios/Pods/ 61 | -------------------------------------------------------------------------------- /src/components/text/Text.tsx: -------------------------------------------------------------------------------- 1 | import React, { ReactNode } from "react"; 2 | import { Text as RNText, TextStyle, StyleSheet } from "react-native"; 3 | import { COLORS } from "@constants/index"; 4 | 5 | type TextProps = { 6 | children?: ReactNode; 7 | type?: "title" | "artist" | "label" | "default"; 8 | style?: TextStyle; 9 | }; 10 | 11 | export const Text = ({ 12 | children, 13 | type = "default", 14 | style, 15 | ...props 16 | }: TextProps) => { 17 | return ( 18 | 19 | {children} 20 | 21 | ); 22 | }; 23 | 24 | export const variants = StyleSheet.create({ 25 | title: { 26 | fontWeight: "600", 27 | fontSize: 15, 28 | lineHeight: 22, 29 | color: COLORS.BLACK, 30 | }, 31 | artist: { 32 | fontSize: 15, 33 | lineHeight: 22, 34 | fontWeight: "500", 35 | color: COLORS.ARTIST_FONT, 36 | }, 37 | label: { 38 | lineHeight: 18, 39 | color: COLORS.INPUT, 40 | }, 41 | default: {}, 42 | }); 43 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: ["module:metro-react-native-babel-preset"], 3 | plugins: [ 4 | [ 5 | "module-resolver", 6 | { 7 | root: ["."], 8 | extensions: [ 9 | ".ios.js", 10 | ".android.js", 11 | ".js", 12 | ".ts", 13 | ".tsx", 14 | ".json", 15 | ], 16 | alias: { 17 | "@api": "./src/api", 18 | "@assets": "./src/assets", 19 | "@components": "./src/components", 20 | "@constants": "./src/constants", 21 | "@model": "./src/model", 22 | "@navigation": "./src/navigation", 23 | "@screens": "./src/screens", 24 | "@utils": "./src/utils", 25 | "@state": "./src/state", 26 | "@contexts": "./src/contexts", 27 | "@hooks": "./src/hooks", 28 | }, 29 | }, 30 | ], 31 | ], 32 | }; 33 | -------------------------------------------------------------------------------- /src/constants/firebase.ts: -------------------------------------------------------------------------------- 1 | export const AUTH_CODES = { 2 | EMAIL_EXISTS: "auth/email-already-in-use", 3 | INVALID_EMAIL: "auth/invalid-email", 4 | WRONG_PASS: "auth/wrong-password", 5 | WEAK_PASS: "auth/weak-password", 6 | TOO_MANY_REQ: "auth/too-many-requests", 7 | }; 8 | 9 | export const AUTH_ALERTS = { 10 | [AUTH_CODES.INVALID_EMAIL]: { 11 | title: "The given email is invalid", 12 | description: "The email address is badly formatted", 13 | }, 14 | [AUTH_CODES.WRONG_PASS]: { 15 | title: "The given password is invalid", 16 | description: "Email / password pair doesn't match", 17 | }, 18 | [AUTH_CODES.WEAK_PASS]: { 19 | title: "The given password is invalid", 20 | description: "Password should be at least 6 characters", 21 | }, 22 | [AUTH_CODES.TOO_MANY_REQ]: { 23 | title: "We have blocked all requests from this device due to unusual activity.", 24 | description: 25 | "Try again later. Access to this account has been temporarily disabled due to many failed login attempts.", 26 | }, 27 | DEFAULT: { 28 | title: "Unknow Error.", 29 | description: "Try again later.", 30 | }, 31 | }; 32 | -------------------------------------------------------------------------------- /ios/GoogleService-Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CLIENT_ID 6 | 780806663702-0butanfsp2bv4mao219dn0iqu2j4cqkk.apps.googleusercontent.com 7 | REVERSED_CLIENT_ID 8 | com.googleusercontent.apps.780806663702-0butanfsp2bv4mao219dn0iqu2j4cqkk 9 | API_KEY 10 | AIzaSyDRO_8exGfdltZOhphR9R9QFqVfTbAX36c 11 | GCM_SENDER_ID 12 | 780806663702 13 | PLIST_VERSION 14 | 1 15 | BUNDLE_ID 16 | kachurin.maxim.art 17 | PROJECT_ID 18 | appart-311f9 19 | STORAGE_BUCKET 20 | appart-311f9.appspot.com 21 | IS_ADS_ENABLED 22 | 23 | IS_ANALYTICS_ENABLED 24 | 25 | IS_APPINVITE_ENABLED 26 | 27 | IS_GCM_ENABLED 28 | 29 | IS_SIGNIN_ENABLED 30 | 31 | GOOGLE_APP_ID 32 | 1:780806663702:ios:1b4e816bef2d377a1c8f37 33 | 34 | -------------------------------------------------------------------------------- /src/components/buttons/RoundButton.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { TouchableOpacity, StyleSheet } from "react-native"; 3 | import { COLORS } from "@constants/index"; 4 | import Icon from "react-native-vector-icons/Feather"; 5 | 6 | type RoundButtonProps = { 7 | onPress: () => void; 8 | type: "close" | "back" | "info"; 9 | }; 10 | 11 | export const RoundButton = ({ onPress, type }: RoundButtonProps) => { 12 | return ( 13 | 14 | 19 | 20 | ); 21 | }; 22 | 23 | const variants = { 24 | close: { 25 | icon: "x", 26 | size: 24, 27 | }, 28 | back: { 29 | icon: "chevron-left", 30 | size: 28, 31 | }, 32 | info: { 33 | icon: "book-open", 34 | size: 24, 35 | }, 36 | }; 37 | 38 | const styles = StyleSheet.create({ 39 | button: { 40 | width: 48, 41 | height: 48, 42 | backgroundColor: COLORS.BUTTON_WRAPPER, 43 | borderRadius: 20, 44 | alignItems: "center", 45 | justifyContent: "center", 46 | }, 47 | }); 48 | -------------------------------------------------------------------------------- /src/components/buttons/Button.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { 3 | TouchableOpacity, 4 | StyleSheet, 5 | View, 6 | ActivityIndicator, 7 | } from "react-native"; 8 | import { Text } from "@components/index"; 9 | import { COLORS } from "@constants/index"; 10 | 11 | type ButtonProps = { 12 | onPress: () => void; 13 | title: string; 14 | isLoading?: boolean; 15 | }; 16 | 17 | export const Button = ({ onPress, title, isLoading }: ButtonProps) => { 18 | return ( 19 | 24 | {isLoading ? ( 25 | 26 | ) : ( 27 | 28 | 29 | {title} 30 | 31 | 32 | )} 33 | 34 | ); 35 | }; 36 | 37 | const styles = StyleSheet.create({ 38 | button: { 39 | height: 46, 40 | backgroundColor: COLORS.ACTIVE, 41 | borderRadius: 4, 42 | alignItems: "center", 43 | justifyContent: "center", 44 | }, 45 | }); 46 | -------------------------------------------------------------------------------- /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: -Xmx1024m -XX:MaxPermSize=256m 13 | org.gradle.jvmargs=-Xmx2048m -XX:MaxPermSize=512m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8 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 | # Automatically convert third-party libraries to use AndroidX 25 | android.enableJetifier=true 26 | 27 | # Version of flipper SDK to use with React Native 28 | FLIPPER_VERSION=0.99.0 29 | -------------------------------------------------------------------------------- /src/App.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { StatusBar } from "react-native"; 3 | import { 4 | SafeAreaProvider, 5 | initialWindowMetrics, 6 | } from "react-native-safe-area-context"; 7 | import { Provider } from "react-redux"; 8 | import { PersistGate } from "redux-persist/integration/react"; 9 | import { persistor, store } from "@state/store/configureStore"; 10 | import { NavigationContainer } from "@react-navigation/native"; 11 | import { HomeNavigator } from "@navigation/index"; 12 | import RNBootSplash from "react-native-bootsplash"; 13 | 14 | const App = () => { 15 | const onReady = () => { 16 | RNBootSplash.hide({ fade: true }); 17 | }; 18 | 19 | return ( 20 | 21 | 22 | 23 | 24 | 29 | 30 | 31 | 32 | 33 | 34 | ); 35 | }; 36 | 37 | export default App; 38 | -------------------------------------------------------------------------------- /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 = "30.0.2" 6 | minSdkVersion = 27 7 | compileSdkVersion = 30 8 | targetSdkVersion = 30 9 | ndkVersion = "21.4.7075529" 10 | } 11 | repositories { 12 | google() 13 | mavenCentral() 14 | } 15 | dependencies { 16 | classpath("com.android.tools.build:gradle:4.2.2") 17 | // NOTE: Do not place your application dependencies here; they belong 18 | // in the individual module build.gradle files 19 | } 20 | } 21 | 22 | allprojects { 23 | repositories { 24 | maven { 25 | // All of React Native (JS, Obj-C sources, Android binaries) is installed from npm 26 | url("$rootDir/../node_modules/react-native/android") 27 | } 28 | maven { 29 | // Android JSC is installed from npm 30 | url("$rootDir/../node_modules/jsc-android/dist") 31 | } 32 | mavenCentral { 33 | // We don't want to fetch react-native from Maven Central as there are 34 | // older versions over there. 35 | content { 36 | excludeGroup "com.facebook.react" 37 | } 38 | } 39 | google() 40 | maven { url 'https://www.jitpack.io' } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/api/api.ts: -------------------------------------------------------------------------------- 1 | import Axios, { AxiosInstance } from "axios"; 2 | import { GetArtworksResponse, SearchResponse } from "@api/api.types"; 3 | import ENDPOINTS from "./endpoints"; 4 | 5 | export class Api { 6 | private static instance: Api; 7 | private axiosInstance: AxiosInstance; 8 | 9 | constructor() { 10 | this.axiosInstance = Axios.create({ 11 | baseURL: "https://api.artic.edu/", 12 | timeout: 30000, 13 | headers: { 14 | "Content-Type": "application/json", 15 | }, 16 | }); 17 | } 18 | 19 | setup() { 20 | this.axiosInstance.interceptors.response.use( 21 | (response) => { 22 | return response.data; 23 | }, 24 | (error) => { 25 | return Promise.reject(error); 26 | } 27 | ); 28 | } 29 | 30 | public static getInstance() { 31 | if (!Api.instance) { 32 | Api.instance = new Api(); 33 | Api.instance.setup(); 34 | } 35 | 36 | return Api.instance; 37 | } 38 | 39 | async getArtworks(params?: any) { 40 | return this.axiosInstance.get( 41 | ENDPOINTS.ARTWORKS, 42 | { params } 43 | ); 44 | } 45 | 46 | async searchArtworks(params?: any) { 47 | return this.axiosInstance.get(ENDPOINTS.SEARCH, { 48 | params: { params }, 49 | }); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /android/app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | 13 | 21 | 22 | 23 | 24 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "allowJs": false, 4 | "allowSyntheticDefaultImports": true, 5 | "experimentalDecorators": true, 6 | "esModuleInterop": true, 7 | "isolatedModules": true, 8 | "jsx": "react-native", 9 | "moduleResolution": "node", 10 | "noImplicitAny": false, 11 | "noImplicitReturns": true, 12 | "noImplicitThis": true, 13 | "noUnusedLocals": true, 14 | "target": "esnext", 15 | "lib": ["es6"], 16 | "skipLibCheck": true, 17 | "resolveJsonModule": true, 18 | "removeComments": true, 19 | "useDefineForClassFields": true, 20 | "baseUrl": ".", 21 | "paths": { 22 | "@api/*": ["src/api/*"], 23 | "@assets/*": ["src/assets/*"], 24 | "@components/*": ["src/components/*"], 25 | "@constants/*": ["src/constants/*"], 26 | "@model/*": ["src/model/*"], 27 | "@navigation/*": ["src/navigation/*"], 28 | "@screens/*": ["src/screens/*"], 29 | "@utils/*": ["src/utils/*"], 30 | "@state/*": ["src/state/*"], 31 | "@contexts/*": ["src/contexts/*"], 32 | "@hooks/*": ["src/hooks/*"] 33 | } 34 | }, 35 | "exclude": ["node_modules"], 36 | "include": [ 37 | "index.js", 38 | "src/**/*", 39 | "metro.config.js", 40 | ".eslintrc.js", 41 | "babel.config.js", 42 | "utils/utils.ts" 43 | ] 44 | } 45 | -------------------------------------------------------------------------------- /src/navigation/navigators/HomeNavigator.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { createSharedElementStackNavigator } from "react-navigation-shared-element"; 3 | import { TransitionPresets } from "@react-navigation/stack"; 4 | import { transitionInterpolation, sharedElementsConfig } from "../config"; 5 | import { DetailsScreen, BrowserScreen } from "@screens/index"; 6 | import TabNavigator from "./TabNavigator"; 7 | import ROUTES from "../routes"; 8 | 9 | const Stack = createSharedElementStackNavigator(); 10 | 11 | export const HomeNavigator = () => { 12 | return ( 13 | 18 | 19 | 28 | 35 | 36 | ); 37 | }; 38 | 39 | export default HomeNavigator; 40 | -------------------------------------------------------------------------------- /android/app/_BUCK: -------------------------------------------------------------------------------- 1 | # To learn about Buck see [Docs](https://buckbuild.com/). 2 | # To run your application with Buck: 3 | # - install Buck 4 | # - `npm start` - to start the packager 5 | # - `cd android` 6 | # - `keytool -genkey -v -keystore keystores/debug.keystore -storepass android -alias androiddebugkey -keypass android -dname "CN=Android Debug,O=Android,C=US"` 7 | # - `./gradlew :app:copyDownloadableDepsToLibs` - make all Gradle compile dependencies available to Buck 8 | # - `buck install -r android/app` - compile, install and run application 9 | # 10 | 11 | load(":build_defs.bzl", "create_aar_targets", "create_jar_targets") 12 | 13 | lib_deps = [] 14 | 15 | create_aar_targets(glob(["libs/*.aar"])) 16 | 17 | create_jar_targets(glob(["libs/*.jar"])) 18 | 19 | android_library( 20 | name = "all-libs", 21 | exported_deps = lib_deps, 22 | ) 23 | 24 | android_library( 25 | name = "app-code", 26 | srcs = glob([ 27 | "src/main/java/**/*.java", 28 | ]), 29 | deps = [ 30 | ":all-libs", 31 | ":build_config", 32 | ":res", 33 | ], 34 | ) 35 | 36 | android_build_config( 37 | name = "build_config", 38 | package = "com.appart", 39 | ) 40 | 41 | android_resource( 42 | name = "res", 43 | package = "com.appart", 44 | res = "src/main/res", 45 | ) 46 | 47 | android_binary( 48 | name = "app", 49 | keystore = "//android/keystores:debug", 50 | manifest = "src/main/AndroidManifest.xml", 51 | package_type = "debug", 52 | deps = [ 53 | ":app-code", 54 | ], 55 | ) 56 | -------------------------------------------------------------------------------- /src/components/criteria/Criteria.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { StyleSheet, View, Text as RNText } from "react-native"; 3 | import { COLORS } from "@constants/colors"; 4 | import { Text } from "@components/text"; 5 | 6 | interface CriteriaProps { 7 | params: Array<{ 8 | title: string; 9 | check: (value?: any) => boolean; 10 | }>; 11 | value: any; 12 | } 13 | 14 | export const Criteria = ({ params, value }: CriteriaProps) => { 15 | return ( 16 | 17 | Passwords must contain at least: 18 | 19 | {params.map((item, i) => { 20 | const passed = item.check(value); 21 | return ( 22 | 23 | 30 | {item.title} 31 | 32 | {i < params.length - 1 && , } 33 | 34 | ); 35 | })} 36 | 37 | 38 | ); 39 | }; 40 | 41 | const styles = StyleSheet.create({ 42 | params: { 43 | alignItems: "center", 44 | flexDirection: "row", 45 | }, 46 | }); 47 | -------------------------------------------------------------------------------- /ios/appArt/Images.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "iPhone_Notifications_20_2x.png", 5 | "idiom" : "iphone", 6 | "scale" : "2x", 7 | "size" : "20x20" 8 | }, 9 | { 10 | "filename" : "iPhone_Notifications_20_3x.png", 11 | "idiom" : "iphone", 12 | "scale" : "3x", 13 | "size" : "20x20" 14 | }, 15 | { 16 | "filename" : "iPhone_Settings_29_2x.png", 17 | "idiom" : "iphone", 18 | "scale" : "2x", 19 | "size" : "29x29" 20 | }, 21 | { 22 | "filename" : "iPhone_Settings_29_3x.png", 23 | "idiom" : "iphone", 24 | "scale" : "3x", 25 | "size" : "29x29" 26 | }, 27 | { 28 | "filename" : "iPhone_Spotlight_40_2x.png", 29 | "idiom" : "iphone", 30 | "scale" : "2x", 31 | "size" : "40x40" 32 | }, 33 | { 34 | "filename" : "iPhone_Spotlight_40_3x.png", 35 | "idiom" : "iphone", 36 | "scale" : "3x", 37 | "size" : "40x40" 38 | }, 39 | { 40 | "filename" : "iPhone_App_60_2x.png", 41 | "idiom" : "iphone", 42 | "scale" : "2x", 43 | "size" : "60x60" 44 | }, 45 | { 46 | "filename" : "iPhone_App_60_3x.png", 47 | "idiom" : "iphone", 48 | "scale" : "3x", 49 | "size" : "60x60" 50 | }, 51 | { 52 | "filename" : "App_store_1024_1x.png", 53 | "idiom" : "ios-marketing", 54 | "scale" : "1x", 55 | "size" : "1024x1024" 56 | } 57 | ], 58 | "info" : { 59 | "author" : "xcode", 60 | "version" : 1 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /ios/appArt/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleDisplayName 8 | appArt 9 | CFBundleExecutable 10 | $(EXECUTABLE_NAME) 11 | CFBundleIdentifier 12 | $(PRODUCT_BUNDLE_IDENTIFIER) 13 | CFBundleInfoDictionaryVersion 14 | 6.0 15 | CFBundleName 16 | $(PRODUCT_NAME) 17 | CFBundlePackageType 18 | APPL 19 | CFBundleShortVersionString 20 | 1.0 21 | CFBundleSignature 22 | ???? 23 | CFBundleVersion 24 | 1 25 | LSRequiresIPhoneOS 26 | 27 | NSAppTransportSecurity 28 | 29 | NSExceptionDomains 30 | 31 | localhost 32 | 33 | NSExceptionAllowsInsecureHTTPLoads 34 | 35 | 36 | 37 | 38 | NSLocationWhenInUseUsageDescription 39 | 40 | UIAppFonts 41 | 42 | Feather.ttf 43 | 44 | UILaunchStoryboardName 45 | BootSplash 46 | UIRequiredDeviceCapabilities 47 | 48 | armv7 49 | 50 | UISupportedInterfaceOrientations 51 | 52 | UIInterfaceOrientationPortrait 53 | 54 | UIViewControllerBasedStatusBarAppearance 55 | 56 | 57 | 58 | -------------------------------------------------------------------------------- /src/screens/home-screen/components/GridItem.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { TouchableOpacity, StyleSheet } from "react-native"; 3 | import { SharedElement } from "react-navigation-shared-element"; 4 | import { useNavigation } from "@react-navigation/native"; 5 | import { COLORS, S_WIDTH, TILE_HEIGHT } from "@constants/index"; 6 | import { getImageURI } from "@api/utils"; 7 | import ROUTES from "@navigation/routes"; 8 | import FastImage from "react-native-fast-image"; 9 | 10 | const GridItem = ({ item, index, smallTileIndex }) => { 11 | const navigation = useNavigation() as any; 12 | 13 | const imageURI = getImageURI(item.image_id); 14 | const onArtworkPress = (index: number) => { 15 | navigation.navigate(ROUTES.DETAILS, { 16 | initialScrollIndex: index, 17 | thumbnailSource: imageURI, 18 | }); 19 | }; 20 | 21 | const height = 22 | index === smallTileIndex ? TILE_HEIGHT.SMALL : TILE_HEIGHT.LARGE; 23 | 24 | return ( 25 | { 29 | onArtworkPress(item.tileIndex); 30 | }} 31 | > 32 | 33 | 38 | 39 | 40 | ); 41 | }; 42 | 43 | const styles = StyleSheet.create({ 44 | image: { 45 | width: S_WIDTH / 2 - 12, 46 | borderRadius: 4, 47 | backgroundColor: COLORS.BUTTON, 48 | marginBottom: 8, 49 | }, 50 | }); 51 | 52 | export default GridItem; 53 | -------------------------------------------------------------------------------- /src/screens/home-screen/components/Spinner.tsx: -------------------------------------------------------------------------------- 1 | import React, { useRef, useEffect } from "react"; 2 | import { Animated, StyleSheet } from "react-native"; 3 | import LottieView from "lottie-react-native"; 4 | import { V_SCALE } from "@constants/index"; 5 | 6 | const Spinner = ({ loading, scrollY, top }) => { 7 | const lottieRef = useRef(null); 8 | 9 | useEffect(() => { 10 | loading && lottieRef.current?.play(21, 40); 11 | }, [loading]); 12 | 13 | const progress = scrollY.interpolate({ 14 | inputRange: [-150, -30, 0], 15 | outputRange: [0.22, 0.05, 0.05], 16 | extrapolate: "clamp", 17 | }); 18 | 19 | const opacity = scrollY.interpolate({ 20 | inputRange: [-100, -40, 0], 21 | outputRange: [1, 0, 0], 22 | extrapolate: "clamp", 23 | }); 24 | 25 | const translateY = scrollY.interpolate({ 26 | inputRange: [-100, -40, 0], 27 | outputRange: [0, -10, -10], 28 | extrapolate: "clamp", 29 | }); 30 | 31 | return ( 32 | 39 | 46 | 47 | ); 48 | }; 49 | 50 | const styles = StyleSheet.create({ 51 | container: { 52 | position: "absolute", 53 | alignSelf: "center", 54 | justifyContent: "center", 55 | height: 50 * V_SCALE, 56 | overflow: "hidden", 57 | }, 58 | }); 59 | 60 | export default Spinner; 61 | -------------------------------------------------------------------------------- /android/app/src/main/res/drawable/rn_edit_text_material.xml: -------------------------------------------------------------------------------- 1 | 2 | 13 | 18 | 19 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /ios/appArtTests/appArtTests.m: -------------------------------------------------------------------------------- 1 | #import 2 | #import 3 | 4 | #import 5 | #import 6 | 7 | #define TIMEOUT_SECONDS 600 8 | #define TEXT_TO_LOOK_FOR @"Welcome to React" 9 | 10 | @interface appArtTests : XCTestCase 11 | 12 | @end 13 | 14 | @implementation appArtTests 15 | 16 | - (BOOL)findSubviewInView:(UIView *)view matching:(BOOL(^)(UIView *view))test 17 | { 18 | if (test(view)) { 19 | return YES; 20 | } 21 | for (UIView *subview in [view subviews]) { 22 | if ([self findSubviewInView:subview matching:test]) { 23 | return YES; 24 | } 25 | } 26 | return NO; 27 | } 28 | 29 | - (void)testRendersWelcomeScreen 30 | { 31 | UIViewController *vc = [[[RCTSharedApplication() delegate] window] rootViewController]; 32 | NSDate *date = [NSDate dateWithTimeIntervalSinceNow:TIMEOUT_SECONDS]; 33 | BOOL foundElement = NO; 34 | 35 | __block NSString *redboxError = nil; 36 | #ifdef DEBUG 37 | RCTSetLogFunction(^(RCTLogLevel level, RCTLogSource source, NSString *fileName, NSNumber *lineNumber, NSString *message) { 38 | if (level >= RCTLogLevelError) { 39 | redboxError = message; 40 | } 41 | }); 42 | #endif 43 | 44 | while ([date timeIntervalSinceNow] > 0 && !foundElement && !redboxError) { 45 | [[NSRunLoop mainRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate dateWithTimeIntervalSinceNow:0.1]]; 46 | [[NSRunLoop mainRunLoop] runMode:NSRunLoopCommonModes beforeDate:[NSDate dateWithTimeIntervalSinceNow:0.1]]; 47 | 48 | foundElement = [self findSubviewInView:vc.view matching:^BOOL(UIView *view) { 49 | if ([view.accessibilityLabel isEqualToString:TEXT_TO_LOOK_FOR]) { 50 | return YES; 51 | } 52 | return NO; 53 | }]; 54 | } 55 | 56 | #ifdef DEBUG 57 | RCTSetLogFunction(RCTDefaultLogFunction); 58 | #endif 59 | 60 | XCTAssertNil(redboxError, @"RedBox error: %@", redboxError); 61 | XCTAssertTrue(foundElement, @"Couldn't find element with text '%@' in %d seconds", TEXT_TO_LOOK_FOR, TIMEOUT_SECONDS); 62 | } 63 | 64 | 65 | @end 66 | -------------------------------------------------------------------------------- /src/state/store/configureStore.ts: -------------------------------------------------------------------------------- 1 | import AsyncStorage from "@react-native-async-storage/async-storage"; 2 | import { Api } from "@api/api"; 3 | import firestore from "@react-native-firebase/firestore"; 4 | import { combineReducers, configureStore } from "@reduxjs/toolkit"; 5 | import { 6 | persistStore, 7 | persistReducer, 8 | FLUSH, 9 | REHYDRATE, 10 | PAUSE, 11 | PERSIST, 12 | PURGE, 13 | REGISTER, 14 | } from "redux-persist"; 15 | import initialState from "@state/store/initialState"; 16 | import { authReducer } from "@state/slices/authSlice"; 17 | import { artworksReducer } from "@state/slices/artworksSlice"; 18 | import { favoritesReducer } from "@state/slices/favoritesSlice"; 19 | 20 | const apiService = Api.getInstance(); 21 | const dbRef = firestore().collection("favorites"); 22 | 23 | const customMiddlewares = [ 24 | /* other middlewares */ 25 | ]; 26 | 27 | const persistConfig = { 28 | key: "root", 29 | version: 1, 30 | storage: AsyncStorage, 31 | }; 32 | 33 | const combinedReducer = combineReducers({ 34 | auth: authReducer, 35 | artworks: artworksReducer, 36 | favorites: favoritesReducer, 37 | }); 38 | 39 | const persistedReducer = persistReducer(persistConfig, combinedReducer); 40 | 41 | export const store = configureStore({ 42 | preloadedState: initialState, 43 | reducer: persistedReducer, 44 | middleware: getDefaultMiddleware => 45 | getDefaultMiddleware({ 46 | serializableCheck: { 47 | ignoredActions: [ 48 | FLUSH, 49 | REHYDRATE, 50 | PAUSE, 51 | PERSIST, 52 | PURGE, 53 | REGISTER, 54 | ], 55 | }, 56 | thunk: { 57 | extraArgument: { apiService, dbRef }, 58 | }, 59 | }).concat(customMiddlewares), 60 | }); 61 | 62 | export const persistor = persistStore(store); 63 | 64 | export type RootState = ReturnType; 65 | export type AppDispatch = typeof store.dispatch; 66 | -------------------------------------------------------------------------------- /src/components/text-field/TextField.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from "react"; 2 | import { View, TextInput, TextInputProps, StyleSheet } from "react-native"; 3 | import { Text } from "@components/index"; 4 | import { COLORS } from "@constants/index"; 5 | 6 | type TextFieldProps = TextInputProps & { 7 | label: string; 8 | inputRef?: React.Ref; 9 | errorMessage?: string; 10 | onFocus?: () => void; 11 | onBlur?: () => void; 12 | }; 13 | 14 | export const TextField = ({ 15 | label, 16 | inputRef, 17 | onFocus, 18 | onBlur, 19 | errorMessage, 20 | ...props 21 | }: TextFieldProps) => { 22 | const [isFocused, setIsFocused] = useState(false); 23 | 24 | const _onFocus = () => { 25 | setIsFocused(true); 26 | typeof onFocus === "function" && onFocus(); 27 | }; 28 | 29 | const _onBlur = () => { 30 | setIsFocused(false); 31 | typeof onBlur === "function" && onBlur(); 32 | }; 33 | 34 | return ( 35 | 36 | {label} 37 | 45 | 53 | 54 | {errorMessage} 55 | 56 | ); 57 | }; 58 | 59 | const styles = StyleSheet.create({ 60 | container: { 61 | height: 38, 62 | width: "100%", 63 | justifyContent: "center", 64 | borderBottomWidth: 1, 65 | }, 66 | input: { 67 | height: "100%", 68 | }, 69 | error: { color: COLORS.ACTIVE, marginTop: 4, marginBottom: 16, height: 16 }, 70 | }); 71 | -------------------------------------------------------------------------------- /src/state/slices/artworksSlice.ts: -------------------------------------------------------------------------------- 1 | import { createSlice, createAsyncThunk } from "@reduxjs/toolkit"; 2 | import defaultInitialState from "@state/store/initialState"; 3 | import { RootState } from "@state/store/configureStore"; 4 | import { GridItem } from "@model/index"; 5 | import { getSearchParams, GetArtworksResponse } from "@api/index"; 6 | import { TThunkAPI } from "../types"; 7 | 8 | export const getArtworks = createAsyncThunk< 9 | GetArtworksResponse, 10 | void, 11 | TThunkAPI 12 | >("artworks/get", async (_, { extra: { apiService } }) => { 13 | const searchResults = await apiService.searchArtworks(getSearchParams()); 14 | const artworkIDs = searchResults.data.map(result => result.id); 15 | 16 | return apiService.getArtworks({ 17 | ids: artworkIDs, 18 | }); 19 | }); 20 | 21 | const extractData = (payload: GetArtworksResponse) => { 22 | return payload.data.map(({ id, title, artist_title, image_id }, i) => { 23 | return { 24 | id, 25 | title, 26 | artist_title, 27 | image_id, 28 | tileIndex: i, 29 | }; 30 | }); 31 | }; 32 | 33 | export interface ArtworksState { 34 | data: GridItem[]; 35 | loading: boolean; 36 | } 37 | 38 | const initialState = defaultInitialState.artworks as ArtworksState; 39 | 40 | export const artworksSlice = createSlice({ 41 | name: "artworks", 42 | initialState, 43 | reducers: {}, 44 | extraReducers: builder => { 45 | builder 46 | .addCase(getArtworks.pending, state => { 47 | state.loading = true; 48 | }) 49 | .addCase(getArtworks.fulfilled, (state, action) => { 50 | state.data = extractData(action.payload); 51 | state.loading = false; 52 | }) 53 | .addCase(getArtworks.rejected, state => { 54 | state.loading = false; 55 | }); 56 | }, 57 | }); 58 | 59 | export const selectArtworks = (state: RootState) => state.artworks; 60 | 61 | export const selectArtworksData = (state: RootState) => state.artworks.data; 62 | 63 | export const artworksReducer = artworksSlice.reducer; 64 | -------------------------------------------------------------------------------- /src/navigation/navigators/TabNavigator.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { createBottomTabNavigator } from "@react-navigation/bottom-tabs"; 3 | import { createSharedElementStackNavigator } from "react-navigation-shared-element"; 4 | import { HomeScreen, FavoritesScreen, AccountScreen } from "@screens/index"; 5 | import Icon from "react-native-vector-icons/Feather"; 6 | import { COLORS } from "@constants/index"; 7 | import ROUTES from "../routes"; 8 | 9 | const Tab = createBottomTabNavigator(); 10 | const SharedStackWrapper = createSharedElementStackNavigator(); 11 | 12 | const SharedHomeScreen = () => { 13 | return ( 14 | 15 | 19 | 20 | ); 21 | }; 22 | 23 | const TabNavigator = () => { 24 | return ( 25 | 32 | ( 37 | 38 | ), 39 | }} 40 | /> 41 | ( 47 | 48 | ), 49 | }} 50 | /> 51 | ( 56 | 57 | ), 58 | }} 59 | /> 60 | 61 | ); 62 | }; 63 | 64 | export default TabNavigator; 65 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "appart", 3 | "version": "0.0.1", 4 | "private": true, 5 | "scripts": { 6 | "android": "react-native run-android", 7 | "ios": "react-native run-ios", 8 | "start": "react-native start", 9 | "test": "jest", 10 | "lint": "eslint . --ext .js,.jsx,.ts,.tsx", 11 | "android-shake": "adb shell input keyevent 82" 12 | }, 13 | "dependencies": { 14 | "@react-native-async-storage/async-storage": "^1.16.1", 15 | "@react-native-firebase/app": "^14.11.0", 16 | "@react-native-firebase/auth": "^14.11.0", 17 | "@react-native-firebase/firestore": "^15.0.0", 18 | "@react-navigation/bottom-tabs": "^6.2.0", 19 | "@react-navigation/native": "^6.0.8", 20 | "@react-navigation/stack": "^6.1.1", 21 | "@reduxjs/toolkit": "^1.8.0", 22 | "axios": "^0.26.0", 23 | "formik": "^2.2.9", 24 | "lottie-ios": "3.2.3", 25 | "lottie-react-native": "^5.0.1", 26 | "react": "17.0.2", 27 | "react-native": "0.67.3", 28 | "react-native-bootsplash": "3.2.6", 29 | "react-native-fast-image": "^8.5.11", 30 | "react-native-gesture-handler": "^2.2.0", 31 | "react-native-safe-area-context": "^4.0.2", 32 | "react-native-screens": "^3.13.0", 33 | "react-native-shared-element": "^0.8.4", 34 | "react-native-vector-icons": "^9.1.0", 35 | "react-native-webview": "^11.18.1", 36 | "react-navigation-shared-element": "^3.1.3", 37 | "react-redux": "^7.2.6", 38 | "redux-persist": "^6.0.0", 39 | "yup": "^0.32.11" 40 | }, 41 | "devDependencies": { 42 | "@babel/core": "^7.12.9", 43 | "@babel/runtime": "^7.12.5", 44 | "@react-native-community/eslint-config": "^2.0.0", 45 | "@types/jest": "^26.0.23", 46 | "@types/react-native": "^0.66.15", 47 | "@types/react-test-renderer": "^17.0.1", 48 | "@typescript-eslint/eslint-plugin": "^5.7.0", 49 | "@typescript-eslint/parser": "^5.7.0", 50 | "babel-jest": "^26.6.3", 51 | "babel-plugin-module-resolver": "^4.1.0", 52 | "eslint": "^7.14.0", 53 | "jest": "^26.6.3", 54 | "metro-react-native-babel-preset": "^0.66.2", 55 | "react-test-renderer": "17.0.2", 56 | "reactotron-react-native": "^5.0.1", 57 | "typescript": "^4.4.4" 58 | }, 59 | "resolutions": { 60 | "@types/react": "^17" 61 | }, 62 | "jest": { 63 | "preset": "react-native", 64 | "moduleFileExtensions": [ 65 | "ts", 66 | "tsx", 67 | "js", 68 | "jsx", 69 | "json", 70 | "node" 71 | ] 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/screens/home-screen/HomeScreen.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useRef } from "react"; 2 | import { ScrollView, Animated, RefreshControl } from "react-native"; 3 | import { HStack, Container } from "@components/index"; 4 | import { useAppSelector, useAppDispatch } from "@hooks/redux"; 5 | import { selectArtworks, getArtworks } from "@state/slices/artworksSlice"; 6 | import { useSafeAreaInsets } from "react-native-safe-area-context"; 7 | import Spinner from "./components/Spinner"; 8 | import GridColumn from "./components/GridColumn"; 9 | import { COLORS, IS_IOS } from "@constants/index"; 10 | 11 | export const HomeScreen = () => { 12 | const dispatch = useAppDispatch(); 13 | const { data: artworks, loading } = useAppSelector(selectArtworks); 14 | const { top } = useSafeAreaInsets(); 15 | 16 | const scrollY = useRef(new Animated.Value(0)).current; 17 | 18 | useEffect(() => { 19 | artworks.length === 0 && dispatch(getArtworks()); 20 | }, []); 21 | 22 | const onRefresh = () => { 23 | dispatch(getArtworks()); 24 | }; 25 | 26 | return ( 27 | 28 | {IS_IOS && } 29 | 39 | } 40 | onScroll={Animated.event( 41 | [ 42 | { 43 | nativeEvent: { 44 | contentOffset: { 45 | y: scrollY, 46 | }, 47 | }, 48 | }, 49 | ], 50 | { useNativeDriver: false } 51 | )} 52 | style={{ paddingTop: top }} 53 | > 54 | 55 | 56 | 57 | 58 | 59 | 60 | ); 61 | }; 62 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | GEM 2 | remote: https://rubygems.org/ 3 | specs: 4 | CFPropertyList (3.0.5) 5 | rexml 6 | activesupport (6.1.4.6) 7 | concurrent-ruby (~> 1.0, >= 1.0.2) 8 | i18n (>= 1.6, < 2) 9 | minitest (>= 5.1) 10 | tzinfo (~> 2.0) 11 | zeitwerk (~> 2.3) 12 | addressable (2.8.0) 13 | public_suffix (>= 2.0.2, < 5.0) 14 | algoliasearch (1.27.5) 15 | httpclient (~> 2.8, >= 2.8.3) 16 | json (>= 1.5.1) 17 | atomos (0.1.3) 18 | claide (1.1.0) 19 | cocoapods (1.11.2) 20 | addressable (~> 2.8) 21 | claide (>= 1.0.2, < 2.0) 22 | cocoapods-core (= 1.11.2) 23 | cocoapods-deintegrate (>= 1.0.3, < 2.0) 24 | cocoapods-downloader (>= 1.4.0, < 2.0) 25 | cocoapods-plugins (>= 1.0.0, < 2.0) 26 | cocoapods-search (>= 1.0.0, < 2.0) 27 | cocoapods-trunk (>= 1.4.0, < 2.0) 28 | cocoapods-try (>= 1.1.0, < 2.0) 29 | colored2 (~> 3.1) 30 | escape (~> 0.0.4) 31 | fourflusher (>= 2.3.0, < 3.0) 32 | gh_inspector (~> 1.0) 33 | molinillo (~> 0.8.0) 34 | nap (~> 1.0) 35 | ruby-macho (>= 1.0, < 3.0) 36 | xcodeproj (>= 1.21.0, < 2.0) 37 | cocoapods-core (1.11.2) 38 | activesupport (>= 5.0, < 7) 39 | addressable (~> 2.8) 40 | algoliasearch (~> 1.0) 41 | concurrent-ruby (~> 1.1) 42 | fuzzy_match (~> 2.0.4) 43 | nap (~> 1.0) 44 | netrc (~> 0.11) 45 | public_suffix (~> 4.0) 46 | typhoeus (~> 1.0) 47 | cocoapods-deintegrate (1.0.5) 48 | cocoapods-downloader (1.5.1) 49 | cocoapods-plugins (1.0.0) 50 | nap 51 | cocoapods-search (1.0.1) 52 | cocoapods-trunk (1.6.0) 53 | nap (>= 0.8, < 2.0) 54 | netrc (~> 0.11) 55 | cocoapods-try (1.2.0) 56 | colored2 (3.1.2) 57 | concurrent-ruby (1.1.9) 58 | escape (0.0.4) 59 | ethon (0.15.0) 60 | ffi (>= 1.15.0) 61 | ffi (1.15.5) 62 | fourflusher (2.3.1) 63 | fuzzy_match (2.0.4) 64 | gh_inspector (1.1.3) 65 | httpclient (2.8.3) 66 | i18n (1.10.0) 67 | concurrent-ruby (~> 1.0) 68 | json (2.6.1) 69 | minitest (5.15.0) 70 | molinillo (0.8.0) 71 | nanaimo (0.3.0) 72 | nap (1.1.0) 73 | netrc (0.11.0) 74 | public_suffix (4.0.6) 75 | rexml (3.2.5) 76 | ruby-macho (2.5.1) 77 | typhoeus (1.4.0) 78 | ethon (>= 0.9.0) 79 | tzinfo (2.0.4) 80 | concurrent-ruby (~> 1.0) 81 | xcodeproj (1.21.0) 82 | CFPropertyList (>= 2.3.3, < 4.0) 83 | atomos (~> 0.1.3) 84 | claide (>= 1.0.2, < 2.0) 85 | colored2 (~> 3.1) 86 | nanaimo (~> 0.3.0) 87 | rexml (~> 3.2.4) 88 | zeitwerk (2.5.4) 89 | PLATFORMS 90 | ruby 91 | DEPENDENCIES 92 | cocoapods (~> 1.11, >= 1.11.2) 93 | RUBY VERSION 94 | ruby 2.7.4p191 95 | BUNDLED WITH 96 | 2.2.27 -------------------------------------------------------------------------------- /android/app/src/main/java/com/appart/MainApplication.java: -------------------------------------------------------------------------------- 1 | package com.appart; 2 | 3 | import android.app.Application; 4 | import android.content.Context; 5 | import com.facebook.react.PackageList; 6 | import com.facebook.react.ReactApplication; 7 | import com.facebook.react.ReactInstanceManager; 8 | import com.facebook.react.ReactNativeHost; 9 | import com.facebook.react.ReactPackage; 10 | import com.facebook.soloader.SoLoader; 11 | import java.lang.reflect.InvocationTargetException; 12 | import java.util.List; 13 | 14 | public class MainApplication extends Application implements ReactApplication { 15 | 16 | private final ReactNativeHost mReactNativeHost = 17 | new ReactNativeHost(this) { 18 | @Override 19 | public boolean getUseDeveloperSupport() { 20 | return BuildConfig.DEBUG; 21 | } 22 | 23 | @Override 24 | protected List getPackages() { 25 | @SuppressWarnings("UnnecessaryLocalVariable") 26 | List packages = new PackageList(this).getPackages(); 27 | // Packages that cannot be autolinked yet can be added manually here, for example: 28 | // packages.add(new MyReactNativePackage()); 29 | return packages; 30 | } 31 | 32 | @Override 33 | protected String getJSMainModuleName() { 34 | return "index"; 35 | } 36 | }; 37 | 38 | @Override 39 | public ReactNativeHost getReactNativeHost() { 40 | return mReactNativeHost; 41 | } 42 | 43 | @Override 44 | public void onCreate() { 45 | super.onCreate(); 46 | SoLoader.init(this, /* native exopackage */ false); 47 | initializeFlipper(this, getReactNativeHost().getReactInstanceManager()); 48 | } 49 | 50 | /** 51 | * Loads Flipper in React Native templates. Call this in the onCreate method with something like 52 | * initializeFlipper(this, getReactNativeHost().getReactInstanceManager()); 53 | * 54 | * @param context 55 | * @param reactInstanceManager 56 | */ 57 | private static void initializeFlipper( 58 | Context context, ReactInstanceManager reactInstanceManager) { 59 | if (BuildConfig.DEBUG) { 60 | try { 61 | /* 62 | We use reflection here to pick up the class that initializes Flipper, 63 | since Flipper library is not available in release mode 64 | */ 65 | Class aClass = Class.forName("com.appart.ReactNativeFlipper"); 66 | aClass 67 | .getMethod("initializeFlipper", Context.class, ReactInstanceManager.class) 68 | .invoke(null, context, reactInstanceManager); 69 | } catch (ClassNotFoundException e) { 70 | e.printStackTrace(); 71 | } catch (NoSuchMethodException e) { 72 | e.printStackTrace(); 73 | } catch (IllegalAccessException e) { 74 | e.printStackTrace(); 75 | } catch (InvocationTargetException e) { 76 | e.printStackTrace(); 77 | } 78 | } 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /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%" == "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%"=="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 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 84 | exit /b 1 85 | 86 | :mainEnd 87 | if "%OS%"=="Windows_NT" endlocal 88 | 89 | :omega 90 | -------------------------------------------------------------------------------- /ios/appArt/AppDelegate.m: -------------------------------------------------------------------------------- 1 | #import "AppDelegate.h" 2 | 3 | // [@react-native-firebase/app] - start 4 | #import 5 | // [@react-native-firebase/app] - end 6 | 7 | // [react-native-bootsplash] - start 8 | #import "RNBootSplash.h" 9 | // [react-native-bootsplash] - end 10 | 11 | #import 12 | #import 13 | #import 14 | 15 | #ifdef FB_SONARKIT_ENABLED 16 | #import 17 | #import 18 | #import 19 | #import 20 | #import 21 | #import 22 | 23 | static void InitializeFlipper(UIApplication *application) { 24 | FlipperClient *client = [FlipperClient sharedClient]; 25 | SKDescriptorMapper *layoutDescriptorMapper = [[SKDescriptorMapper alloc] initWithDefaults]; 26 | [client addPlugin:[[FlipperKitLayoutPlugin alloc] initWithRootNode:application withDescriptorMapper:layoutDescriptorMapper]]; 27 | [client addPlugin:[[FKUserDefaultsPlugin alloc] initWithSuiteName:nil]]; 28 | [client addPlugin:[FlipperKitReactPlugin new]]; 29 | [client addPlugin:[[FlipperKitNetworkPlugin alloc] initWithNetworkAdapter:[SKIOSNetworkAdapter new]]]; 30 | [client start]; 31 | } 32 | #endif 33 | 34 | @implementation AppDelegate 35 | 36 | - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions 37 | { 38 | 39 | // [@react-native-firebase/app] - start 40 | if ([FIRApp defaultApp] == nil) { 41 | [FIRApp configure]; 42 | } 43 | // [@react-native-firebase/app] - end 44 | 45 | #ifdef FB_SONARKIT_ENABLED 46 | InitializeFlipper(application); 47 | #endif 48 | 49 | RCTBridge *bridge = [[RCTBridge alloc] initWithDelegate:self launchOptions:launchOptions]; 50 | RCTRootView *rootView = [[RCTRootView alloc] initWithBridge:bridge 51 | moduleName:@"appArt" 52 | initialProperties:nil]; 53 | 54 | if (@available(iOS 13.0, *)) { 55 | rootView.backgroundColor = [UIColor systemBackgroundColor]; 56 | } else { 57 | rootView.backgroundColor = [UIColor whiteColor]; 58 | } 59 | 60 | self.window = [[UIWindow alloc] initWithFrame:[UIScreen mainScreen].bounds]; 61 | UIViewController *rootViewController = [UIViewController new]; 62 | rootViewController.view = rootView; 63 | self.window.rootViewController = rootViewController; 64 | [self.window makeKeyAndVisible]; 65 | 66 | // [react-native-bootsplash] - start 67 | [RNBootSplash initWithStoryboard:@"BootSplash" rootView:rootView]; 68 | // [react-native-bootsplash] - end 69 | 70 | return YES; 71 | } 72 | 73 | - (NSURL *)sourceURLForBridge:(RCTBridge *)bridge 74 | { 75 | #if DEBUG 76 | return [[RCTBundleURLProvider sharedSettings] jsBundleURLForBundleRoot:@"index" fallbackResource:nil]; 77 | #else 78 | return [[NSBundle mainBundle] URLForResource:@"main" withExtension:@"jsbundle"]; 79 | #endif 80 | } 81 | 82 | @end 83 | -------------------------------------------------------------------------------- /ios/appArt/BootSplash.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 | -------------------------------------------------------------------------------- /src/state/slices/authSlice.ts: -------------------------------------------------------------------------------- 1 | import { createSlice, createAsyncThunk } from "@reduxjs/toolkit"; 2 | import defaultInitialState from "@state/store/initialState"; 3 | import { RootState } from "@state/store/configureStore"; 4 | import { mergeItems } from "@state/slices/favoritesSlice"; 5 | import auth from "@react-native-firebase/auth"; 6 | import { FormValues } from "@screens/index"; 7 | import { TThunkAPI, User } from "../types"; 8 | import { AUTH_CODES } from "@constants/firebase"; 9 | 10 | export const login = createAsyncThunk( 11 | "auth/login", 12 | async ({ email, password }, { dispatch, rejectWithValue }) => { 13 | try { 14 | return await auth() 15 | .createUserWithEmailAndPassword(email, password) 16 | .then(UserCredential => { 17 | dispatch(mergeItems()); 18 | return UserCredential.user.toJSON() as User; 19 | }); 20 | } catch (e) { 21 | if (e.code === AUTH_CODES.EMAIL_EXISTS) { 22 | return await auth() 23 | .signInWithEmailAndPassword(email, password) 24 | .then(UserCredential => { 25 | dispatch(mergeItems()); 26 | return UserCredential.user.toJSON() as User; 27 | }) 28 | .catch(e => { 29 | console.log("Firebase:signIn:ERROR", e.code); 30 | return rejectWithValue(e.code); 31 | }); 32 | } else { 33 | console.log("Firebase:createUser:ERROR", e.code); 34 | return rejectWithValue(e.code); 35 | } 36 | } 37 | }, 38 | ); 39 | 40 | export const logout = createAsyncThunk( 41 | "auth/logout", 42 | async (_, { rejectWithValue }) => { 43 | try { 44 | return await auth().signOut(); 45 | } catch (e) { 46 | return rejectWithValue(e.code); 47 | } 48 | }, 49 | ); 50 | 51 | export interface AuthState { 52 | user: User | null; 53 | loading: boolean; 54 | } 55 | 56 | const initialState = defaultInitialState.auth as AuthState; 57 | 58 | export const authSlice = createSlice({ 59 | name: "auth", 60 | initialState, 61 | reducers: {}, 62 | extraReducers: builder => { 63 | builder 64 | .addCase(login.fulfilled, (state, action) => { 65 | state.user = action.payload; 66 | state.loading = false; 67 | }) 68 | .addCase(logout.fulfilled, state => { 69 | state.user = null; 70 | state.loading = false; 71 | }) 72 | .addMatcher( 73 | action => action.type.endsWith("/pending"), 74 | state => { 75 | state.loading = true; 76 | }, 77 | ) 78 | .addMatcher( 79 | action => action.type.endsWith("/rejected"), 80 | state => { 81 | state.loading = false; 82 | }, 83 | ); 84 | }, 85 | }); 86 | 87 | export const selectAuth = (state: RootState) => state.auth; 88 | 89 | export const selectCurrentUser = (state: RootState) => state.auth.user; 90 | 91 | export const authReducer = authSlice.reducer; 92 | -------------------------------------------------------------------------------- /android/app/src/debug/java/com/appart/ReactNativeFlipper.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Facebook, Inc. and its 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.appart; 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.react.ReactFlipperPlugin; 21 | import com.facebook.flipper.plugins.sharedpreferences.SharedPreferencesFlipperPlugin; 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 | public class ReactNativeFlipper { 28 | public static void initializeFlipper(Context context, ReactInstanceManager reactInstanceManager) { 29 | if (FlipperUtils.shouldEnableFlipper(context)) { 30 | final FlipperClient client = AndroidFlipperClient.getInstance(context); 31 | 32 | client.addPlugin(new InspectorFlipperPlugin(context, DescriptorMapping.withDefaults())); 33 | client.addPlugin(new ReactFlipperPlugin()); 34 | client.addPlugin(new DatabasesFlipperPlugin(context)); 35 | client.addPlugin(new SharedPreferencesFlipperPlugin(context)); 36 | client.addPlugin(CrashReporterPlugin.getInstance()); 37 | 38 | NetworkFlipperPlugin networkFlipperPlugin = new NetworkFlipperPlugin(); 39 | NetworkingModule.setCustomClientBuilder( 40 | new NetworkingModule.CustomClientBuilder() { 41 | @Override 42 | public void apply(OkHttpClient.Builder builder) { 43 | builder.addNetworkInterceptor(new FlipperOkhttpInterceptor(networkFlipperPlugin)); 44 | } 45 | }); 46 | client.addPlugin(networkFlipperPlugin); 47 | client.start(); 48 | 49 | // Fresco Plugin needs to ensure that ImagePipelineFactory is initialized 50 | // Hence we run if after all native modules have been initialized 51 | ReactContext reactContext = reactInstanceManager.getCurrentReactContext(); 52 | if (reactContext == null) { 53 | reactInstanceManager.addReactInstanceEventListener( 54 | new ReactInstanceManager.ReactInstanceEventListener() { 55 | @Override 56 | public void onReactContextInitialized(ReactContext reactContext) { 57 | reactInstanceManager.removeReactInstanceEventListener(this); 58 | reactContext.runOnNativeModulesQueueThread( 59 | new Runnable() { 60 | @Override 61 | public void run() { 62 | client.addPlugin(new FrescoFlipperPlugin()); 63 | } 64 | }); 65 | } 66 | }); 67 | } else { 68 | client.addPlugin(new FrescoFlipperPlugin()); 69 | } 70 | } 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /ios/appArt.xcodeproj/xcshareddata/xcschemes/appArt.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 | -------------------------------------------------------------------------------- /src/screens/favorites-screen/FavoritesScreen.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { 3 | View, 4 | FlatList, 5 | ListRenderItem, 6 | TouchableOpacity, 7 | StyleSheet, 8 | LayoutAnimation, 9 | } from "react-native"; 10 | import FastImage from "react-native-fast-image"; 11 | import { Container, Text } from "@components/index"; 12 | import { useAppSelector, useAppDispatch } from "@hooks/redux"; 13 | import { favoritesSelector, removeItem } from "@state/slices/favoritesSlice"; 14 | import { SafeAreaView } from "react-native-safe-area-context"; 15 | import { FavoriteItem } from "@model/index"; 16 | import { getImageURI } from "@api/utils"; 17 | import Icon from "react-native-vector-icons/Feather"; 18 | import { COLORS, S_WIDTH, BASE_URL } from "@constants/index"; 19 | import ROUTES from "@navigation/routes"; 20 | 21 | export const FavoritesScreen = ({ navigation }) => { 22 | const favoritesData = useAppSelector(favoritesSelector.selectAll); 23 | const dispatch = useAppDispatch(); 24 | 25 | const onArtworkPress = (id: number) => { 26 | navigation.navigate(ROUTES.BROWSER, { 27 | url: BASE_URL + id, 28 | }); 29 | }; 30 | 31 | const onRemovePress = (id: number) => { 32 | LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut); 33 | dispatch(removeItem({ id })); 34 | }; 35 | 36 | const renderItem: ListRenderItem = ({ item }) => { 37 | return ( 38 | 39 | onArtworkPress(item.id)} 42 | style={styles.touchWrapper}> 43 | 48 | 49 | {item.title} 50 | {item.artist_title} 51 | 52 | 53 | onRemovePress(item.id)} 55 | style={styles.deleteButton}> 56 | 57 | 58 | 59 | ); 60 | }; 61 | 62 | return ( 63 | 64 | 65 | 66 | 67 | 68 | ); 69 | }; 70 | 71 | const styles = StyleSheet.create({ 72 | favItem: { 73 | width: S_WIDTH, 74 | marginVertical: 10, 75 | paddingHorizontal: 16, 76 | flexDirection: "row", 77 | justifyContent: "space-between", 78 | }, 79 | touchWrapper: { 80 | flexDirection: "row", 81 | }, 82 | labelContainer: { 83 | width: S_WIDTH - 200, 84 | paddingHorizontal: 16, 85 | marginTop: 4, 86 | }, 87 | image: { 88 | height: 100, 89 | width: 100, 90 | borderRadius: 6, 91 | }, 92 | deleteButton: { 93 | width: 44, 94 | height: 44, 95 | alignItems: "center", 96 | justifyContent: "center", 97 | borderRadius: 20, 98 | backgroundColor: COLORS.ICON_BG, 99 | }, 100 | }); 101 | -------------------------------------------------------------------------------- /src/state/slices/favoritesSlice.ts: -------------------------------------------------------------------------------- 1 | import { 2 | createSlice, 3 | createEntityAdapter, 4 | createAsyncThunk, 5 | } from "@reduxjs/toolkit"; 6 | import { RootState } from "@state/store/configureStore"; 7 | import { TThunkAPI } from "../types"; 8 | import { FavoriteItem } from "@model/index"; 9 | import defaultInitialState from "@state/store/initialState"; 10 | import auth from "@react-native-firebase/auth"; 11 | import firestore from "@react-native-firebase/firestore"; 12 | 13 | export const addItem = createAsyncThunk( 14 | "favorites/add", 15 | async (item, { dispatch, extra: { dbRef } }) => { 16 | const uid = auth().currentUser?.uid; 17 | if (uid) { 18 | dbRef.doc(uid).update({ 19 | [item.id]: item, 20 | }); 21 | } 22 | dispatch(addOne(item)); 23 | }, 24 | ); 25 | 26 | export const removeItem = createAsyncThunk( 27 | "favorites/remove", 28 | async ({ id }, { dispatch, extra: { dbRef } }) => { 29 | const uid = auth().currentUser?.uid; 30 | if (uid) { 31 | dbRef.doc(uid).update({ 32 | [id]: firestore.FieldValue.delete(), 33 | }); 34 | } 35 | dispatch(removeOne(id)); 36 | }, 37 | ); 38 | 39 | export const mergeItems = createAsyncThunk( 40 | "favorites/merge", 41 | async (_, { dispatch, getState, extra: { dbRef } }) => { 42 | const rootState = getState() as RootState; 43 | const uid = auth().currentUser?.uid; 44 | if (uid) { 45 | const { entities: entitiesApp, ids: idsApp } = rootState.favorites; 46 | dbRef 47 | .doc(uid) 48 | .get() 49 | .then(async documentSnapshot => { 50 | if (documentSnapshot.exists) { 51 | const entitiesFirebase = documentSnapshot.data(); 52 | const idsFirebase = Object.keys(entitiesFirebase); 53 | 54 | let result: FavoriteItem[] = Object.values(entitiesApp); 55 | 56 | idsFirebase.forEach(id => { 57 | !idsApp.includes(Number(id)) && 58 | result.push(entitiesFirebase[id]); 59 | }); 60 | 61 | result.sort((a, b) => a.timestamp - b.timestamp); 62 | 63 | idsApp.length > 0 && 64 | dbRef.doc(uid).update({ ...entitiesApp }); 65 | dispatch(setAll(result)); 66 | console.log("Firebase document updated"); 67 | } else { 68 | dbRef.doc(uid).set(entitiesApp); 69 | console.log("Firebase document created"); 70 | } 71 | }); 72 | } 73 | }, 74 | ); 75 | 76 | export interface FavoritesState { 77 | ids: number[]; 78 | entities: {}; 79 | } 80 | 81 | const initialState = defaultInitialState.favorites as FavoritesState; 82 | 83 | const favoritesAdapter = createEntityAdapter({ 84 | selectId: item => item.id, 85 | }); 86 | 87 | export const favoritesSlice = createSlice({ 88 | name: "favorites", 89 | initialState, 90 | reducers: { 91 | addOne: favoritesAdapter.addOne, 92 | removeOne: favoritesAdapter.removeOne, 93 | setAll: favoritesAdapter.setAll, 94 | }, 95 | }); 96 | 97 | const { addOne, removeOne, setAll } = favoritesSlice.actions; 98 | 99 | export const favoritesSelector = favoritesAdapter.getSelectors( 100 | state => state.favorites, 101 | ); 102 | 103 | export const favoritesReducer = favoritesSlice.reducer; 104 | -------------------------------------------------------------------------------- /src/api/api.types.ts: -------------------------------------------------------------------------------- 1 | type SearchItem = { 2 | _score: number; 3 | thumbnail: { 4 | alt_text: string; 5 | width: number; 6 | lqip: string; 7 | height: number; 8 | }; 9 | api_model: string; 10 | is_boosted: boolean; 11 | api_link: string; 12 | id: number; 13 | title: string; 14 | timestamp: string; 15 | }; 16 | 17 | export type SearchResponse = { 18 | pagination: { 19 | total: number; 20 | limit: number; 21 | offset: number; 22 | total_pages: number; 23 | current_page: number; 24 | }; 25 | data: SearchItem[]; 26 | }; 27 | 28 | type ArtworkDataItem = { 29 | id: number; 30 | api_model: string; 31 | api_link: string; 32 | is_boosted: boolean; 33 | title: string; 34 | alt_titles: any; 35 | thumbnail: { 36 | lqip: string; 37 | width: number; 38 | height: number; 39 | alt_text: string; 40 | }; 41 | main_reference_number: string; 42 | has_not_been_viewed_much: boolean; 43 | boost_rank: any; 44 | date_start: number; 45 | date_end: number; 46 | date_display: string; 47 | date_qualifier_title: string; 48 | date_qualifier_id: any; 49 | artist_display: string; 50 | place_of_origin: string; 51 | dimensions: string; 52 | medium_display: string; 53 | inscriptions: string; 54 | credit_line: string; 55 | publication_history: string; 56 | exhibition_history: string; 57 | provenance_text: string; 58 | publishing_verification_level: string; 59 | internal_department_id: number; 60 | fiscal_year: number; 61 | fiscal_year_deaccession: any; 62 | is_public_domain: boolean; 63 | is_zoomable: boolean; 64 | max_zoom_window_size: number; 65 | copyright_notice: any; 66 | has_multimedia_resources: boolean; 67 | has_educational_resources: boolean; 68 | colorfulness: number; 69 | color: { 70 | h: number; 71 | l: number; 72 | s: number; 73 | percentage: number; 74 | population: number; 75 | }; 76 | latitude?: number; 77 | longitude?: number; 78 | latlon?: string; 79 | is_on_view: boolean; 80 | on_loan_display: string; 81 | gallery_title: any; 82 | gallery_id: any; 83 | artwork_type_title: string; 84 | artwork_type_id: number; 85 | department_title: string; 86 | department_id: string; 87 | artist_id: number; 88 | artist_title: string; 89 | alt_artist_ids: Array; 90 | artist_ids: Array; 91 | artist_titles: Array; 92 | category_ids: Array; 93 | category_titles: Array; 94 | artwork_catalogue_ids: Array; 95 | term_titles: Array; 96 | style_id: string; 97 | style_title: string; 98 | alt_style_ids: Array; 99 | style_ids: Array; 100 | style_titles: Array; 101 | classification_id: string; 102 | classification_title: string; 103 | alt_classification_ids: Array; 104 | classification_ids: Array; 105 | classification_titles: Array; 106 | subject_id: string; 107 | alt_subject_ids: Array; 108 | subject_ids: Array; 109 | subject_titles: Array; 110 | material_id: string; 111 | alt_material_ids: Array; 112 | material_ids: Array; 113 | material_titles: Array; 114 | technique_id?: string; 115 | alt_technique_ids: Array; 116 | technique_ids: Array; 117 | technique_titles: Array; 118 | theme_titles: Array; 119 | image_id: string; 120 | alt_image_ids: Array; 121 | document_ids: Array; 122 | sound_ids: Array; 123 | video_ids: Array; 124 | text_ids: Array; 125 | section_ids: Array; 126 | section_titles: Array; 127 | site_ids: Array; 128 | suggest_autocomplete_all: Array<{ 129 | input: Array; 130 | contexts: { 131 | groupings: Array; 132 | }; 133 | weight?: number; 134 | }>; 135 | last_updated_source: string; 136 | last_updated: string; 137 | timestamp: string; 138 | suggest_autocomplete_boosted?: string; 139 | }; 140 | 141 | export type GetArtworksResponse = { 142 | pagination: { 143 | total: number; 144 | limit: number; 145 | offset: number; 146 | total_pages: number; 147 | current_page: number; 148 | next_url: string; 149 | }; 150 | data: ArtworkDataItem[]; 151 | }; 152 | -------------------------------------------------------------------------------- /src/screens/details-screen/DetailsScreen.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useRef } from "react"; 2 | import { 3 | Animated, 4 | FlatList, 5 | StyleSheet, 6 | View, 7 | ListRenderItem, 8 | NativeSyntheticEvent, 9 | NativeScrollEvent, 10 | } from "react-native"; 11 | import FastImage from "react-native-fast-image"; 12 | import { useAppSelector, useAppDispatch } from "@hooks/redux"; 13 | import { 14 | addItem, 15 | removeItem, 16 | favoritesSelector, 17 | } from "@state/slices/favoritesSlice"; 18 | import { selectArtworksData } from "@state/slices/artworksSlice"; 19 | import { 20 | Container, 21 | Text, 22 | RoundButton, 23 | FavoriteButton, 24 | } from "@components/index"; 25 | import { COLORS, S_WIDTH, S_HEIGT, BASE_URL } from "@constants/index"; 26 | import { SharedElement } from "react-navigation-shared-element"; 27 | import { GridItem } from "@model/index"; 28 | import { getImageURI } from "@api/utils"; 29 | import ROUTES from "@navigation/routes"; 30 | import { getFavoriteItem } from "@utils/helpers"; 31 | 32 | export const DetailsScreen = ({ navigation, route }) => { 33 | const artworks = useAppSelector(selectArtworksData); 34 | const favoritesIds = useAppSelector(favoritesSelector.selectIds); 35 | const dispatch = useAppDispatch(); 36 | 37 | const { initialScrollIndex } = route.params; 38 | const currentScrollIndex = useRef(initialScrollIndex); 39 | const opacity = useRef(new Animated.Value(0)).current; 40 | 41 | useEffect(() => { 42 | setTimeout(() => { 43 | Animated.timing(opacity, { 44 | toValue: 1, 45 | duration: 250, 46 | useNativeDriver: true, 47 | }).start(); 48 | }, 500); 49 | }, []); 50 | 51 | const onBackButtonPress = () => { 52 | navigation.navigate(ROUTES.HOME, { 53 | currentScrollIndex: currentScrollIndex.current, 54 | }); 55 | }; 56 | 57 | const onInfoButtonPress = () => { 58 | navigation.navigate(ROUTES.BROWSER, { 59 | url: BASE_URL + artworks[currentScrollIndex.current].id, 60 | }); 61 | }; 62 | 63 | const onFavoritePress = (item: GridItem, active: boolean) => { 64 | dispatch(active ? removeItem(item) : addItem(getFavoriteItem(item))); 65 | }; 66 | 67 | const renderItem: ListRenderItem = ({ item, index }) => { 68 | const active = favoritesIds.includes(item.id); 69 | return ( 70 | 71 | 72 | 77 | 78 | 79 | 80 | 81 | {item.title} 82 | {item.artist_title} 83 | 84 | onFavoritePress(item, active)} 86 | active={active} 87 | /> 88 | 89 | 90 | ); 91 | }; 92 | 93 | const getItemLayout = (_, index: number) => ({ 94 | length: S_WIDTH, 95 | offset: S_WIDTH * index, 96 | index, 97 | }); 98 | 99 | const onMomentumScrollEnd = (e: NativeSyntheticEvent) => 100 | (currentScrollIndex.current = Math.round( 101 | e.nativeEvent.contentOffset.x / S_WIDTH, 102 | )); 103 | 104 | return ( 105 | 106 | 116 | 117 | 118 | 119 | 120 | 121 | ); 122 | }; 123 | 124 | const styles = StyleSheet.create({ 125 | container: { backgroundColor: COLORS.DEFAULT }, 126 | image: { 127 | height: S_HEIGT, 128 | width: S_WIDTH, 129 | }, 130 | backButtonWrapper: { 131 | position: "absolute", 132 | flexDirection: "row", 133 | justifyContent: "space-between", 134 | top: 48, 135 | left: 16, 136 | right: 16, 137 | }, 138 | labelWrapper: { 139 | position: "absolute", 140 | backgroundColor: COLORS.BG, 141 | width: S_WIDTH - 48, 142 | borderRadius: 8, 143 | bottom: 44, 144 | marginHorizontal: 24, 145 | paddingHorizontal: 24, 146 | paddingVertical: 8, 147 | flexDirection: "row", 148 | justifyContent: "space-between", 149 | alignItems: "center", 150 | }, 151 | labelText: { 152 | width: S_WIDTH / 1.5, 153 | }, 154 | }); 155 | -------------------------------------------------------------------------------- /android/gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | # 4 | # Copyright 2015 the original author or 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 UN*X 22 | ## 23 | ############################################################################## 24 | 25 | # Attempt to set APP_HOME 26 | # Resolve links: $0 may be a link 27 | PRG="$0" 28 | # Need this for relative symlinks. 29 | while [ -h "$PRG" ] ; do 30 | ls=`ls -ld "$PRG"` 31 | link=`expr "$ls" : '.*-> \(.*\)$'` 32 | if expr "$link" : '/.*' > /dev/null; then 33 | PRG="$link" 34 | else 35 | PRG=`dirname "$PRG"`"/$link" 36 | fi 37 | done 38 | SAVED="`pwd`" 39 | cd "`dirname \"$PRG\"`/" >/dev/null 40 | APP_HOME="`pwd -P`" 41 | cd "$SAVED" >/dev/null 42 | 43 | APP_NAME="Gradle" 44 | APP_BASE_NAME=`basename "$0"` 45 | 46 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 47 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' 48 | 49 | # Use the maximum available, or set MAX_FD != -1 to use that value. 50 | MAX_FD="maximum" 51 | 52 | warn () { 53 | echo "$*" 54 | } 55 | 56 | die () { 57 | echo 58 | echo "$*" 59 | echo 60 | exit 1 61 | } 62 | 63 | # OS specific support (must be 'true' or 'false'). 64 | cygwin=false 65 | msys=false 66 | darwin=false 67 | nonstop=false 68 | case "`uname`" in 69 | CYGWIN* ) 70 | cygwin=true 71 | ;; 72 | Darwin* ) 73 | darwin=true 74 | ;; 75 | MINGW* ) 76 | msys=true 77 | ;; 78 | NONSTOP* ) 79 | nonstop=true 80 | ;; 81 | esac 82 | 83 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 84 | 85 | 86 | # Determine the Java command to use to start the JVM. 87 | if [ -n "$JAVA_HOME" ] ; then 88 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 89 | # IBM's JDK on AIX uses strange locations for the executables 90 | JAVACMD="$JAVA_HOME/jre/sh/java" 91 | else 92 | JAVACMD="$JAVA_HOME/bin/java" 93 | fi 94 | if [ ! -x "$JAVACMD" ] ; then 95 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 96 | 97 | Please set the JAVA_HOME variable in your environment to match the 98 | location of your Java installation." 99 | fi 100 | else 101 | JAVACMD="java" 102 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 103 | 104 | Please set the JAVA_HOME variable in your environment to match the 105 | location of your Java installation." 106 | fi 107 | 108 | # Increase the maximum file descriptors if we can. 109 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then 110 | MAX_FD_LIMIT=`ulimit -H -n` 111 | if [ $? -eq 0 ] ; then 112 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 113 | MAX_FD="$MAX_FD_LIMIT" 114 | fi 115 | ulimit -n $MAX_FD 116 | if [ $? -ne 0 ] ; then 117 | warn "Could not set maximum file descriptor limit: $MAX_FD" 118 | fi 119 | else 120 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 121 | fi 122 | fi 123 | 124 | # For Darwin, add options to specify how the application appears in the dock 125 | if $darwin; then 126 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 127 | fi 128 | 129 | # For Cygwin or MSYS, switch paths to Windows format before running java 130 | if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then 131 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 132 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 133 | 134 | JAVACMD=`cygpath --unix "$JAVACMD"` 135 | 136 | # We build the pattern for arguments to be converted via cygpath 137 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 138 | SEP="" 139 | for dir in $ROOTDIRSRAW ; do 140 | ROOTDIRS="$ROOTDIRS$SEP$dir" 141 | SEP="|" 142 | done 143 | OURCYGPATTERN="(^($ROOTDIRS))" 144 | # Add a user-defined pattern to the cygpath arguments 145 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 146 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 147 | fi 148 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 149 | i=0 150 | for arg in "$@" ; do 151 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 152 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 153 | 154 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 155 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 156 | else 157 | eval `echo args$i`="\"$arg\"" 158 | fi 159 | i=`expr $i + 1` 160 | done 161 | case $i in 162 | 0) set -- ;; 163 | 1) set -- "$args0" ;; 164 | 2) set -- "$args0" "$args1" ;; 165 | 3) set -- "$args0" "$args1" "$args2" ;; 166 | 4) set -- "$args0" "$args1" "$args2" "$args3" ;; 167 | 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 168 | 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 169 | 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 170 | 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 171 | 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 172 | esac 173 | fi 174 | 175 | # Escape application args 176 | save () { 177 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done 178 | echo " " 179 | } 180 | APP_ARGS=`save "$@"` 181 | 182 | # Collect all arguments for the java command, following the shell quoting and substitution rules 183 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" 184 | 185 | exec "$JAVACMD" "$@" 186 | -------------------------------------------------------------------------------- /src/screens/account-screen/AccountScreen.tsx: -------------------------------------------------------------------------------- 1 | import React, { useRef, useState } from "react"; 2 | import { 3 | View, 4 | Keyboard, 5 | KeyboardAvoidingView, 6 | TouchableWithoutFeedback, 7 | LayoutAnimation, 8 | Platform, 9 | StyleSheet, 10 | } from "react-native"; 11 | import { 12 | Container, 13 | Text, 14 | TextField, 15 | Button, 16 | Criteria, 17 | } from "@components/index"; 18 | import { useAppSelector, useAppDispatch } from "@hooks/redux"; 19 | import { selectAuth, login, logout } from "@state/slices/authSlice"; 20 | import { isRejected } from "@reduxjs/toolkit"; 21 | import { AUTH_ALERTS } from "@constants/firebase"; 22 | import { showAlert } from "@utils/helpers"; 23 | import { useFormik } from "formik"; 24 | import * as yup from "yup"; 25 | 26 | export type FormValues = { 27 | email: string; 28 | password: string; 29 | }; 30 | 31 | export const AccountScreen = () => { 32 | const inputRefs = useRef({}); 33 | const dispatch = useAppDispatch(); 34 | 35 | const [passwordInFocus, setPasswordInFocus] = useState(false); 36 | const { user, loading } = useAppSelector(selectAuth); 37 | 38 | const onSubmit = async (values: FormValues, { resetForm }) => { 39 | const result = await dispatch(login(values)); 40 | if (isRejected(result)) { 41 | const alert = AUTH_ALERTS[result.payload]; 42 | showAlert(alert ? alert : AUTH_ALERTS.DEFAULT); 43 | } else { 44 | resetForm(); 45 | } 46 | }; 47 | 48 | const onSignOut = () => { 49 | dispatch(logout()); 50 | }; 51 | 52 | const formik = useFormik({ 53 | initialValues: { 54 | email: "", 55 | password: "", 56 | }, 57 | onSubmit, 58 | validateOnMount: true, 59 | validationSchema: yup.object().shape({ 60 | email: yup 61 | .string() 62 | .email("Email must be valid") 63 | .required("Email is required"), 64 | password: yup 65 | .string() 66 | .required("Password is required") 67 | .test( 68 | "is-valid-password", 69 | "Password is invalid", 70 | value => value?.length >= 6 && !!value?.match(/[A-Z]/), 71 | ), 72 | }), 73 | }); 74 | 75 | const onPasswordFocusToggle = state => { 76 | LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut); 77 | setPasswordInFocus(state); 78 | }; 79 | 80 | return ( 81 | 82 | {!user ? ( 83 | { 85 | Keyboard.dismiss(); 86 | }}> 87 | 91 | 92 | WELCOME! 93 | Login/signup to save your 94 | favorite atrwork in the cloud. 95 | 96 | 97 | 104 | inputRefs.current["password"]?.focus() 105 | } 106 | autoCapitalize="none" 107 | keyboardType="email-address" 108 | returnKeyType="next" 109 | errorMessage={ 110 | formik.touched.email && 111 | (formik.errors?.email as string) 112 | } 113 | /> 114 | { 116 | inputRefs.current["password"] = ref; 117 | }} 118 | label="Password" 119 | placeholder="Enter a secure password" 120 | placeholderTextColor="gray" 121 | value={formik.values.password} 122 | onChangeText={formik.handleChange("password")} 123 | onSubmitEditing={formik.handleSubmit} 124 | autoCapitalize="none" 125 | returnKeyType="done" 126 | secureTextEntry 127 | blurOnSubmit={false} 128 | onFocus={() => onPasswordFocusToggle(true)} 129 | onBlur={() => onPasswordFocusToggle(false)} 130 | errorMessage={ 131 | formik.touched.password && 132 | (formik.errors?.password as string) 133 | } 134 | /> 135 | {passwordInFocus && ( 136 | v.length >= 6, 141 | }, 142 | { 143 | title: "1 upper case letter", 144 | check: v => v.match(/[A-Z]/), 145 | }, 146 | ]} 147 | value={formik.values.password} 148 | /> 149 | )} 150 | 151 |