├── .eslintignore ├── .eslintrc.json ├── .gitignore ├── .prettierrc.json ├── .vscode └── settings.json ├── .yarnrc.yml ├── App.tsx ├── Documents └── Images │ ├── Diagram.png │ └── Screens.png ├── README.md ├── app.json ├── assets ├── adaptive-icon.png ├── favicon.png ├── fonts │ ├── Pretendard-Bold.ttf │ ├── Pretendard-ExtraBold.ttf │ └── Pretendard-Regular.ttf ├── icon.png ├── icons │ ├── ic_arrow_left.svg │ ├── ic_arrow_left_white.svg │ ├── ic_chat.svg │ ├── ic_close.png │ ├── ic_close.svg │ ├── ic_send.svg │ ├── ic_tab_archives.svg │ ├── ic_tab_archives_active.svg │ ├── ic_tab_home.svg │ ├── ic_tab_home_active.svg │ ├── ic_tab_settings.svg │ └── ic_tab_settings_active.svg ├── images │ ├── img_empty.png │ └── thumbnail_placeholder_upload.png ├── langs │ ├── en.json │ └── ko.json ├── lotties │ └── onboard.json └── splash.png ├── babel.config.js ├── metro.config.js ├── package.json ├── src ├── components │ ├── customAppbar │ │ ├── index.tsx │ │ └── styles.ts │ ├── customHeader │ │ ├── index.tsx │ │ └── styles.ts │ ├── divider │ │ └── index.tsx │ ├── emptyScreen │ │ ├── index.tsx │ │ └── styles.ts │ ├── floatingButton │ │ ├── index.tsx │ │ └── styles.ts │ └── keyboardAwareScrollView │ │ ├── index.tsx │ │ └── styles.ts ├── configs │ ├── api_keys.ts │ ├── recoil_keys.ts │ ├── screen_types.ts │ ├── socket_keys.ts │ └── tasks_types.ts ├── dtos │ ├── chat_dtos.ts │ ├── location_dtos.ts │ ├── post_dtos.ts │ ├── socket_dtos.ts │ └── upload_dtos.ts ├── helpers │ └── device_utils.ts ├── hooks │ ├── use_chat_socket.ts │ └── use_location_socket.ts ├── i18n │ ├── i18n.ts │ ├── index.ts │ └── translate.ts ├── main.tsx ├── navigation │ ├── permissoned_navigator.tsx │ ├── root_navigator.tsx │ ├── tabs │ │ ├── archives_navigator.tsx │ │ ├── home_navigator.tsx │ │ └── settings_navigator.tsx │ └── un_permissoned_navigator.tsx ├── recoils │ ├── location_states.ts │ └── settings_states.ts ├── screens │ ├── archives │ │ ├── archiveItem │ │ │ ├── index.tsx │ │ │ └── styles.ts │ │ ├── index.tsx │ │ └── styles.ts │ ├── home │ │ ├── custom_map_styles.ts │ │ ├── index.tsx │ │ └── styles.ts │ ├── infoDevelopers │ │ ├── index.tsx │ │ └── styles.ts │ ├── onBoarding │ │ ├── index.tsx │ │ └── styles.ts │ ├── postChat │ │ ├── chatBalloon │ │ │ ├── balloon_left.tsx │ │ │ ├── balloon_right.tsx │ │ │ ├── index.tsx │ │ │ └── styles.ts │ │ ├── index.tsx │ │ ├── interfaces.ts │ │ └── styles.ts │ ├── postDetail │ │ ├── index.tsx │ │ ├── interfaces.ts │ │ └── styles.ts │ ├── postWrite │ │ ├── index.tsx │ │ ├── interfaces.ts │ │ └── styles.ts │ └── settings │ │ ├── index.tsx │ │ └── styles.ts ├── services │ ├── posts_service.ts │ └── upload_service.ts ├── themes.ts ├── types.d.ts └── utils │ └── recoil_utils.ts └── tsconfig.json /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | babel.config.js 3 | .expo/ 4 | dist/ 5 | npm-debug.* 6 | *.jks 7 | *.p8 8 | *.p12 9 | *.key 10 | *.mobileprovision 11 | *.orig.* 12 | web-build/ 13 | web-report/ 14 | metro.config.js 15 | 16 | # MacOS 17 | .DS_Store 18 | 19 | # Yarn 20 | yarn-error.log 21 | yarn.lock 22 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "env": { 4 | "es6": true, 5 | "node": true 6 | }, 7 | "parser": "@typescript-eslint/parser", 8 | "parserOptions": { 9 | "ecmaFeatures": { 10 | "jsx": true 11 | }, 12 | "ecmaVersion": 2020, 13 | "sourceType": "module", 14 | "project": "./tsconfig.json" 15 | }, 16 | "settings": { 17 | "import/parsers": { 18 | "@typescript-eslint/parser": [".ts", ".tsx"] 19 | }, 20 | "import/resolver": { 21 | "typescript": { 22 | "alwaysTryTypes": true 23 | } 24 | } 25 | }, 26 | "plugins": ["@typescript-eslint", "react", "react-hooks", "prettier"], 27 | "extends": [ 28 | "airbnb", 29 | "airbnb/hooks", 30 | "airbnb-typescript", 31 | "plugin:import/recommended", 32 | "plugin:import/typescript", 33 | "plugin:jsx-a11y/recommended", 34 | "plugin:@typescript-eslint/recommended", 35 | "prettier", 36 | "plugin:prettier/recommended" 37 | ], 38 | "rules": { 39 | "prettier/prettier": "error", 40 | "import/prefer-default-export": "off", 41 | "import/no-unresolved": "off", 42 | "react/display-name": "off", 43 | "react/jsx-filename-extension": ["warn", { 44 | "extensions": [ 45 | ".ts", 46 | ".tsx" 47 | ] 48 | }], 49 | "react/prop-types": "off", 50 | "react/react-in-jsx-scope": "off", 51 | "react/style-prop-object": "off", 52 | "react-hooks/exhaustive-deps": "off", 53 | "@typescript-eslint/ban-ts-comment": "off", 54 | "@typescript-eslint/ban-ts-ignore": "off", 55 | "@typescript-eslint/no-non-null-assertion": "off", 56 | "@typescript-eslint/return-await": "off" 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | .expo/ 3 | dist/ 4 | npm-debug.* 5 | *.jks 6 | *.p8 7 | *.p12 8 | *.key 9 | *.mobileprovision 10 | *.orig.* 11 | web-build/ 12 | 13 | # macOS 14 | .DS_Store 15 | 16 | # yarn 17 | .yarn 18 | yarn.lock 19 | 20 | -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "trailingComma": "all", 3 | "arrowParens": "always", 4 | "singleQuote": true, 5 | "printWidth": 120, 6 | "semi": true, 7 | "useTabs": false 8 | } 9 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "eslint.validate": [ 3 | "javascript", 4 | "javascriptreact", 5 | "typescript", 6 | "typescriptreact" 7 | ], 8 | "editor.codeActionsOnSave": { 9 | "source.fixAll": true 10 | }, 11 | "[javascript]": { 12 | "editor.defaultFormatter": "esbenp.prettier-vscode" 13 | }, 14 | "[javascriptreact]": { 15 | "editor.defaultFormatter": "esbenp.prettier-vscode" 16 | }, 17 | "[typescript]": { 18 | "editor.defaultFormatter": "esbenp.prettier-vscode" 19 | }, 20 | "[typescriptreact]": { 21 | "editor.defaultFormatter": "esbenp.prettier-vscode" 22 | }, 23 | "javascript.preferences.importModuleSpecifier": "relative", 24 | "typescript.preferences.importModuleSpecifier": "relative", 25 | "prettier.configPath": ".prettierrc.json" 26 | } 27 | -------------------------------------------------------------------------------- /.yarnrc.yml: -------------------------------------------------------------------------------- 1 | nodeLinker: node-modules 2 | -------------------------------------------------------------------------------- /App.tsx: -------------------------------------------------------------------------------- 1 | import 'react-native-gesture-handler'; 2 | import 'text-encoding'; 3 | import { LogBox } from 'react-native'; 4 | import App from './src/main'; 5 | 6 | LogBox.ignoreLogs(['Setting a timer']); 7 | 8 | export default App; 9 | -------------------------------------------------------------------------------- /Documents/Images/Diagram.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JaeWangL/blabla-mobile/c25785b8472241c91896c39eced27f6fdbade9db/Documents/Images/Diagram.png -------------------------------------------------------------------------------- /Documents/Images/Screens.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JaeWangL/blabla-mobile/c25785b8472241c91896c39eced27f6fdbade9db/Documents/Images/Screens.png -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # BlaBla Mobile 2 | 3 | BlaBla is location-based social media service. This release intends to share a simplified version of Expo apps and Microservice architecture 4 | 5 | # BlaBla Repos 6 | 7 | For running this BlaBla mobile application, I built several back-end services with microservice architecture with Azure. You 8 | can find all BlaBla repos in the following locations: 9 | 10 | - [Mobile](https://github.com/JaeWangL/blabla-mobile) 11 | - [Service - Posts API](https://github.com/JaeWangL/blabla-api-posts) 12 | - [Service - Location API](https://github.com/JaeWangL/blabla-api-location) 13 | - [Service - Chat API](https://github.com/JaeWangL/blabla-api-chat) 14 | - [Service - Upload API](https://github.com/JaeWangL/blabla-api-upload) 15 | - [Infrastructure - Config](https://github.com/JaeWangL/blabla-infra-config) 16 | - [Infrastructure - Discovery](https://github.com/JaeWangL/blabla-infra-discovery) 17 | 18 | **Note:** This document is about the apps using **React Native Expo**. 19 | 20 | # Application Diagram 21 | 22 |

23 | 24 |

25 | 26 | # Application Screens 27 | 28 |

29 | 30 |

31 | -------------------------------------------------------------------------------- /app.json: -------------------------------------------------------------------------------- 1 | { 2 | "expo": { 3 | "name": "BlaBla", 4 | "slug": "BlaBla", 5 | "version": "1.0.1", 6 | "orientation": "portrait", 7 | "icon": "./assets/icon.png", 8 | "splash": { 9 | "image": "./assets/splash.png", 10 | "resizeMode": "contain", 11 | "backgroundColor": "#ffffff" 12 | }, 13 | "updates": { 14 | "fallbackToCacheTimeout": 0 15 | }, 16 | "assetBundlePatterns": [ 17 | "**/*" 18 | ], 19 | "userInterfaceStyle": "automatic", 20 | "plugins": [ 21 | [ 22 | "expo-image-picker", 23 | { 24 | "photosPermission": "The app accesses your photos to let you share them with your friends." 25 | } 26 | ] 27 | ], 28 | "ios": { 29 | "bundleIdentifier": "com.hahahoho.blabla", 30 | "buildNumber": "1.0.0", 31 | "supportsTablet": true, 32 | "userInterfaceStyle": "light", 33 | "infoPlist": { 34 | "UIBackgroundModes": [ 35 | "location", 36 | "fetch" 37 | ], 38 | "NSLocationAlwaysAndWhenInUseUsageDescription": "This app uses the location to track your location.", 39 | "NSLocationAlwaysUsageDescription": "This app uses the location to track your location.", 40 | "NSLocationWhenInUseUsageDescription": "This app uses the location to track your location.", 41 | "NSPhotoLibraryUsageDescription": "The app accesses your photos to let you share them with your friends.", 42 | "NSCameraUsageDescription": "Allow $(PRODUCT_NAME) to access your camera", 43 | "NSMicrophoneUsageDescription": "Allow $(PRODUCT_NAME) to access your microphone" 44 | }, 45 | "config": { 46 | "googleMapsApiKey": "AIzaSyCAVnFWJrSnur-DoHtp0uRIux8Iv3kBSaM" 47 | } 48 | }, 49 | "android": { 50 | "adaptiveIcon": { 51 | "foregroundImage": "./assets/adaptive-icon.png", 52 | "backgroundColor": "#FFFFFF" 53 | }, 54 | "userInterfaceStyle": "dark", 55 | "permissions": [ 56 | "ACCESS_COARSE_LOCATION", 57 | "ACCESS_FINE_LOCATION", 58 | "ACCESS_BACKGROUND_LOCATION", 59 | "android.permission.CAMERA", 60 | "android.permission.READ_EXTERNAL_STORAGE", 61 | "android.permission.WRITE_EXTERNAL_STORAGE", 62 | "android.permission.RECORD_AUDIO" 63 | ], 64 | "package": "com.hahahoho.blabla", 65 | "versionCode": 1, 66 | "config": { 67 | "googleMaps": { 68 | "apiKey": "AIzaSyCAVnFWJrSnur-DoHtp0uRIux8Iv3kBSaM" 69 | } 70 | } 71 | }, 72 | "web": { 73 | "favicon": "./assets/favicon.png" 74 | }, 75 | "packagerOpts": { 76 | "config": "metro.config.js" 77 | } 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /assets/adaptive-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JaeWangL/blabla-mobile/c25785b8472241c91896c39eced27f6fdbade9db/assets/adaptive-icon.png -------------------------------------------------------------------------------- /assets/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JaeWangL/blabla-mobile/c25785b8472241c91896c39eced27f6fdbade9db/assets/favicon.png -------------------------------------------------------------------------------- /assets/fonts/Pretendard-Bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JaeWangL/blabla-mobile/c25785b8472241c91896c39eced27f6fdbade9db/assets/fonts/Pretendard-Bold.ttf -------------------------------------------------------------------------------- /assets/fonts/Pretendard-ExtraBold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JaeWangL/blabla-mobile/c25785b8472241c91896c39eced27f6fdbade9db/assets/fonts/Pretendard-ExtraBold.ttf -------------------------------------------------------------------------------- /assets/fonts/Pretendard-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JaeWangL/blabla-mobile/c25785b8472241c91896c39eced27f6fdbade9db/assets/fonts/Pretendard-Regular.ttf -------------------------------------------------------------------------------- /assets/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JaeWangL/blabla-mobile/c25785b8472241c91896c39eced27f6fdbade9db/assets/icon.png -------------------------------------------------------------------------------- /assets/icons/ic_arrow_left.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /assets/icons/ic_arrow_left_white.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /assets/icons/ic_chat.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /assets/icons/ic_close.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JaeWangL/blabla-mobile/c25785b8472241c91896c39eced27f6fdbade9db/assets/icons/ic_close.png -------------------------------------------------------------------------------- /assets/icons/ic_close.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /assets/icons/ic_send.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /assets/icons/ic_tab_archives.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /assets/icons/ic_tab_archives_active.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /assets/icons/ic_tab_home.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /assets/icons/ic_tab_home_active.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /assets/icons/ic_tab_settings.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /assets/icons/ic_tab_settings_active.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /assets/images/img_empty.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JaeWangL/blabla-mobile/c25785b8472241c91896c39eced27f6fdbade9db/assets/images/img_empty.png -------------------------------------------------------------------------------- /assets/images/thumbnail_placeholder_upload.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JaeWangL/blabla-mobile/c25785b8472241c91896c39eced27f6fdbade9db/assets/images/thumbnail_placeholder_upload.png -------------------------------------------------------------------------------- /assets/langs/en.json: -------------------------------------------------------------------------------- 1 | { 2 | "common": { 3 | "cancel": "Cancel", 4 | "noData": "There are no data to dispaly", 5 | "ok": "OK" 6 | }, 7 | "button": { 8 | "acceptPermissionLocation": "Start Journey" 9 | }, 10 | "dialogs": { 11 | "newPostTitle": "New post alert", 12 | "permissionDeniedTitle": "Permission Denied", 13 | "permissionRequestLocation": "You have to accept location permissions", 14 | "permissionRequestPhotos": "Please accept photos permissions", 15 | "postCreatingFailed": "Error is occured when writing post. Please try again later", 16 | "postCreatingFailedTitle": "Creation Failed", 17 | "postCreatingSucceed": "New post is created", 18 | "postCreatingSucceedTitle": "Post Created", 19 | "rateLimitedTitle": "Too many requests", 20 | "rateLimitedDesc": "Please try again later after {{retryRemainingS}}s", 21 | "uploadErrorTitle": "Upload Error", 22 | "uploadErrorExceed": "Maximum thumbnail size up to 5MB", 23 | "uploadErrorUnk": "Error is caused. Please try again later" 24 | }, 25 | "placeholder": { 26 | "chatInput": "Please input messages ..." 27 | }, 28 | "archives": { 29 | "title": "Archives" 30 | }, 31 | "settings": { 32 | "groupInfo": "App Information", 33 | "infoDeveloper": "Developer Information", 34 | "infoVersion": "App Version", 35 | "title": "Settings" 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /assets/langs/ko.json: -------------------------------------------------------------------------------- 1 | { 2 | "common": { 3 | "cancel": "취소", 4 | "noData": "표시할 데이터가 없습니다.", 5 | "ok": "확인" 6 | }, 7 | "button": { 8 | "acceptPermissionLocation": "동의하고 시작하기" 9 | }, 10 | "dialogs": { 11 | "newPostTitle": "새로운 포스트 알림", 12 | "permissionDeniedTitle": "권한 에러", 13 | "permissionRequestLocation": "앱의 원활한 사용을 위해 위치정보 권한을 허용해 주세요.", 14 | "permissionRequestPhotos": "앱의 원활한 사용을 위해 사진 권한을 허용해 주세요.", 15 | "postCreatingFailed": "포스트를 작성하던 중 오류가 발생했습니다. 잠시 후 다시 시도해 주세요.", 16 | "postCreatingFailedTitle": "등록 에러", 17 | "postCreatingSucceed": "새로운 포스트가 등록되었습니다.", 18 | "postCreatingSucceedTitle": "등록 완료", 19 | "rateLimitedTitle": "연속클릭 알림", 20 | "rateLimitedDesc": "{{retryRemainingS}}초 후 다시 시도해 주세요.", 21 | "uploadErrorTitle": "업로드 오류", 22 | "uploadErrorExceed": "썸네일은 최대 5MB 까지 업로드 할 수 있습니다.", 23 | "uploadErrorUnk": "파일을 업로드 하던 중 오류가 발생했습니다. 잠시 후 다시 시도해 주세요." 24 | }, 25 | "placeholder": { 26 | "chatInput": "메세지를 입력해 주세요 ..." 27 | }, 28 | "archives": { 29 | "title": "모아보기" 30 | }, 31 | "settings": { 32 | "groupInfo": "앱 정보", 33 | "infoDeveloper": "개발자 정보", 34 | "infoVersion": "앱 버전", 35 | "title": "설정" 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /assets/lotties/onboard.json: -------------------------------------------------------------------------------- 1 | { 2 | "v":"5.5.5", 3 | "fr":25, 4 | "ip":0, 5 | "op":20, 6 | "w":140, 7 | "h":140, 8 | "nm":"Top 1", 9 | "ddd":0, 10 | "assets":[ 11 | 12 | ], 13 | "layers":[ 14 | { 15 | "ddd":0, 16 | "ind":1, 17 | "ty":4, 18 | "nm":"1-1", 19 | "sr":1, 20 | "ks":{ 21 | "o":{ 22 | "a":1, 23 | "k":[ 24 | { 25 | "i":{ 26 | "x":[ 27 | 0.833 28 | ], 29 | "y":[ 30 | 0.833 31 | ] 32 | }, 33 | "o":{ 34 | "x":[ 35 | 0.167 36 | ], 37 | "y":[ 38 | 0.167 39 | ] 40 | }, 41 | "t":0, 42 | "s":[ 43 | 100 44 | ] 45 | }, 46 | { 47 | "t":60, 48 | "s":[ 49 | 0 50 | ] 51 | } 52 | ], 53 | "ix":11 54 | }, 55 | "r":{ 56 | "a":0, 57 | "k":0, 58 | "ix":10 59 | }, 60 | "p":{ 61 | "a":0, 62 | "k":[ 63 | 70.678, 64 | 70.928, 65 | 0 66 | ], 67 | "ix":2 68 | }, 69 | "a":{ 70 | "a":0, 71 | "k":[ 72 | 51.178, 73 | 214.428, 74 | 0 75 | ], 76 | "ix":1 77 | }, 78 | "s":{ 79 | "a":1, 80 | "k":[ 81 | { 82 | "i":{ 83 | "x":[ 84 | 0.833, 85 | 0.833, 86 | 0.833 87 | ], 88 | "y":[ 89 | 0.833, 90 | 0.833, 91 | 0.833 92 | ] 93 | }, 94 | "o":{ 95 | "x":[ 96 | 0.167, 97 | 0.167, 98 | 0.167 99 | ], 100 | "y":[ 101 | 0.167, 102 | 0.167, 103 | 0.167 104 | ] 105 | }, 106 | "t":0, 107 | "s":[ 108 | 100, 109 | 100, 110 | 100 111 | ] 112 | }, 113 | { 114 | "i":{ 115 | "x":[ 116 | 0.833, 117 | 0.833, 118 | 0.833 119 | ], 120 | "y":[ 121 | 0.833, 122 | 0.833, 123 | 0.833 124 | ] 125 | }, 126 | "o":{ 127 | "x":[ 128 | 0.167, 129 | 0.167, 130 | 0.167 131 | ], 132 | "y":[ 133 | 0.167, 134 | 0.167, 135 | 0.167 136 | ] 137 | }, 138 | "t":20, 139 | "s":[ 140 | 150, 141 | 150, 142 | 100 143 | ] 144 | }, 145 | { 146 | "i":{ 147 | "x":[ 148 | 0.833, 149 | 0.833, 150 | 0.833 151 | ], 152 | "y":[ 153 | 0.833, 154 | 0.833, 155 | 0.833 156 | ] 157 | }, 158 | "o":{ 159 | "x":[ 160 | 0.167, 161 | 0.167, 162 | 0.167 163 | ], 164 | "y":[ 165 | 0.167, 166 | 0.167, 167 | 0.167 168 | ] 169 | }, 170 | "t":40, 171 | "s":[ 172 | 200, 173 | 200, 174 | 100 175 | ] 176 | }, 177 | { 178 | "t":60, 179 | "s":[ 180 | 250, 181 | 250, 182 | 100 183 | ] 184 | } 185 | ], 186 | "ix":6 187 | } 188 | }, 189 | "ao":0, 190 | "shapes":[ 191 | { 192 | "ty":"gr", 193 | "it":[ 194 | { 195 | "ind":0, 196 | "ty":"sh", 197 | "ix":1, 198 | "ks":{ 199 | "a":0, 200 | "k":{ 201 | "i":[ 202 | [ 203 | 13.767, 204 | 0 205 | ], 206 | [ 207 | 0, 208 | -13.767 209 | ], 210 | [ 211 | -13.767, 212 | 0 213 | ], 214 | [ 215 | 0, 216 | 13.767 217 | ] 218 | ], 219 | "o":[ 220 | [ 221 | -13.767, 222 | 0 223 | ], 224 | [ 225 | 0, 226 | 13.767 227 | ], 228 | [ 229 | 13.767, 230 | 0 231 | ], 232 | [ 233 | 0, 234 | -13.767 235 | ] 236 | ], 237 | "v":[ 238 | [ 239 | -6.25, 240 | -28.428 241 | ], 242 | [ 243 | -31.178, 244 | -3.5 245 | ], 246 | [ 247 | -6.25, 248 | 21.428 249 | ], 250 | [ 251 | 18.678, 252 | -3.5 253 | ] 254 | ], 255 | "c":true 256 | }, 257 | "ix":2 258 | }, 259 | "nm":"路径 1", 260 | "mn":"ADBE Vector Shape - Group", 261 | "hd":false 262 | }, 263 | { 264 | "ty":"st", 265 | "c":{ 266 | "a":0, 267 | "k":[ 268 | 0.29411764705882354, 269 | 0.2235294117647059, 270 | 0.9372549019607843, 271 | 1 272 | ], 273 | "ix":3 274 | }, 275 | "o":{ 276 | "a":0, 277 | "k":100, 278 | "ix":4 279 | }, 280 | "w":{ 281 | "a":1, 282 | "k":[ 283 | { 284 | "i":{ 285 | "x":[ 286 | 0.833 287 | ], 288 | "y":[ 289 | 0.833 290 | ] 291 | }, 292 | "o":{ 293 | "x":[ 294 | 0.167 295 | ], 296 | "y":[ 297 | 0.167 298 | ] 299 | }, 300 | "t":0, 301 | "s":[ 302 | 6 303 | ] 304 | }, 305 | { 306 | "i":{ 307 | "x":[ 308 | 0.833 309 | ], 310 | "y":[ 311 | 0.833 312 | ] 313 | }, 314 | "o":{ 315 | "x":[ 316 | 0.167 317 | ], 318 | "y":[ 319 | 0.167 320 | ] 321 | }, 322 | "t":20, 323 | "s":[ 324 | 5 325 | ] 326 | }, 327 | { 328 | "i":{ 329 | "x":[ 330 | 0.833 331 | ], 332 | "y":[ 333 | 0.833 334 | ] 335 | }, 336 | "o":{ 337 | "x":[ 338 | 0.167 339 | ], 340 | "y":[ 341 | 0.167 342 | ] 343 | }, 344 | "t":40, 345 | "s":[ 346 | 4 347 | ] 348 | }, 349 | { 350 | "t":60, 351 | "s":[ 352 | 3 353 | ] 354 | } 355 | ], 356 | "ix":5 357 | }, 358 | "lc":1, 359 | "lj":1, 360 | "ml":4, 361 | "bm":0, 362 | "nm":"描边 1", 363 | "mn":"ADBE Vector Graphic - Stroke", 364 | "hd":false 365 | }, 366 | { 367 | "ty":"tr", 368 | "p":{ 369 | "a":0, 370 | "k":[ 371 | 57.428, 372 | 217.928 373 | ], 374 | "ix":2 375 | }, 376 | "a":{ 377 | "a":0, 378 | "k":[ 379 | 0, 380 | 0 381 | ], 382 | "ix":1 383 | }, 384 | "s":{ 385 | "a":0, 386 | "k":[ 387 | 100, 388 | 100 389 | ], 390 | "ix":3 391 | }, 392 | "r":{ 393 | "a":0, 394 | "k":0, 395 | "ix":6 396 | }, 397 | "o":{ 398 | "a":0, 399 | "k":100, 400 | "ix":7 401 | }, 402 | "sk":{ 403 | "a":0, 404 | "k":0, 405 | "ix":4 406 | }, 407 | "sa":{ 408 | "a":0, 409 | "k":0, 410 | "ix":5 411 | }, 412 | "nm":"变换" 413 | } 414 | ], 415 | "nm":"椭圆 1", 416 | "np":3, 417 | "cix":2, 418 | "bm":0, 419 | "ix":1, 420 | "mn":"ADBE Vector Group", 421 | "hd":false 422 | } 423 | ], 424 | "ip":0, 425 | "op":1526, 426 | "st":0, 427 | "bm":0 428 | }, 429 | { 430 | "ddd":0, 431 | "ind":2, 432 | "ty":4, 433 | "nm":"1-2", 434 | "sr":1, 435 | "ks":{ 436 | "o":{ 437 | "a":1, 438 | "k":[ 439 | { 440 | "i":{ 441 | "x":[ 442 | 0.833 443 | ], 444 | "y":[ 445 | 0.833 446 | ] 447 | }, 448 | "o":{ 449 | "x":[ 450 | 0.167 451 | ], 452 | "y":[ 453 | 0.167 454 | ] 455 | }, 456 | "t":20, 457 | "s":[ 458 | 100 459 | ] 460 | }, 461 | { 462 | "t":80, 463 | "s":[ 464 | 0 465 | ] 466 | } 467 | ], 468 | "ix":11 469 | }, 470 | "r":{ 471 | "a":0, 472 | "k":0, 473 | "ix":10 474 | }, 475 | "p":{ 476 | "a":0, 477 | "k":[ 478 | 613.678, 479 | 1432.428, 480 | 0 481 | ], 482 | "ix":2 483 | }, 484 | "a":{ 485 | "a":0, 486 | "k":[ 487 | 51.178, 488 | 214.428, 489 | 0 490 | ], 491 | "ix":1 492 | }, 493 | "s":{ 494 | "a":1, 495 | "k":[ 496 | { 497 | "i":{ 498 | "x":[ 499 | 0.833, 500 | 0.833, 501 | 0.833 502 | ], 503 | "y":[ 504 | 0.833, 505 | 0.833, 506 | 0.833 507 | ] 508 | }, 509 | "o":{ 510 | "x":[ 511 | 0.167, 512 | 0.167, 513 | 0.167 514 | ], 515 | "y":[ 516 | 0.167, 517 | 0.167, 518 | 0.167 519 | ] 520 | }, 521 | "t":20, 522 | "s":[ 523 | 100, 524 | 100, 525 | 100 526 | ] 527 | }, 528 | { 529 | "i":{ 530 | "x":[ 531 | 0.833, 532 | 0.833, 533 | 0.833 534 | ], 535 | "y":[ 536 | 0.833, 537 | 0.833, 538 | 0.833 539 | ] 540 | }, 541 | "o":{ 542 | "x":[ 543 | 0.167, 544 | 0.167, 545 | 0.167 546 | ], 547 | "y":[ 548 | 0.167, 549 | 0.167, 550 | 0.167 551 | ] 552 | }, 553 | "t":40, 554 | "s":[ 555 | 150, 556 | 150, 557 | 100 558 | ] 559 | }, 560 | { 561 | "i":{ 562 | "x":[ 563 | 0.833, 564 | 0.833, 565 | 0.833 566 | ], 567 | "y":[ 568 | 0.833, 569 | 0.833, 570 | 0.833 571 | ] 572 | }, 573 | "o":{ 574 | "x":[ 575 | 0.167, 576 | 0.167, 577 | 0.167 578 | ], 579 | "y":[ 580 | 0.167, 581 | 0.167, 582 | 0.167 583 | ] 584 | }, 585 | "t":60, 586 | "s":[ 587 | 200, 588 | 200, 589 | 100 590 | ] 591 | }, 592 | { 593 | "t":80, 594 | "s":[ 595 | 250, 596 | 250, 597 | 100 598 | ] 599 | } 600 | ], 601 | "ix":6 602 | } 603 | }, 604 | "ao":0, 605 | "shapes":[ 606 | { 607 | "ty":"gr", 608 | "it":[ 609 | { 610 | "ind":0, 611 | "ty":"sh", 612 | "ix":1, 613 | "ks":{ 614 | "a":0, 615 | "k":{ 616 | "i":[ 617 | [ 618 | 13.767, 619 | 0 620 | ], 621 | [ 622 | 0, 623 | -13.767 624 | ], 625 | [ 626 | -13.767, 627 | 0 628 | ], 629 | [ 630 | 0, 631 | 13.767 632 | ] 633 | ], 634 | "o":[ 635 | [ 636 | -13.767, 637 | 0 638 | ], 639 | [ 640 | 0, 641 | 13.767 642 | ], 643 | [ 644 | 13.767, 645 | 0 646 | ], 647 | [ 648 | 0, 649 | -13.767 650 | ] 651 | ], 652 | "v":[ 653 | [ 654 | -6.25, 655 | -28.428 656 | ], 657 | [ 658 | -31.178, 659 | -3.5 660 | ], 661 | [ 662 | -6.25, 663 | 21.428 664 | ], 665 | [ 666 | 18.678, 667 | -3.5 668 | ] 669 | ], 670 | "c":true 671 | }, 672 | "ix":2 673 | }, 674 | "nm":"路径 1", 675 | "mn":"ADBE Vector Shape - Group", 676 | "hd":false 677 | }, 678 | { 679 | "ty":"st", 680 | "c":{ 681 | "a":0, 682 | "k":[ 683 | 0.29411764705882354, 684 | 0.2235294117647059, 685 | 0.9372549019607843, 686 | 1 687 | ], 688 | "ix":3 689 | }, 690 | "o":{ 691 | "a":0, 692 | "k":100, 693 | "ix":4 694 | }, 695 | "w":{ 696 | "a":1, 697 | "k":[ 698 | { 699 | "i":{ 700 | "x":[ 701 | 0.833 702 | ], 703 | "y":[ 704 | 0.833 705 | ] 706 | }, 707 | "o":{ 708 | "x":[ 709 | 0.167 710 | ], 711 | "y":[ 712 | 0.167 713 | ] 714 | }, 715 | "t":20, 716 | "s":[ 717 | 6 718 | ] 719 | }, 720 | { 721 | "i":{ 722 | "x":[ 723 | 0.833 724 | ], 725 | "y":[ 726 | 0.833 727 | ] 728 | }, 729 | "o":{ 730 | "x":[ 731 | 0.167 732 | ], 733 | "y":[ 734 | 0.167 735 | ] 736 | }, 737 | "t":40, 738 | "s":[ 739 | 5 740 | ] 741 | }, 742 | { 743 | "i":{ 744 | "x":[ 745 | 0.833 746 | ], 747 | "y":[ 748 | 0.833 749 | ] 750 | }, 751 | "o":{ 752 | "x":[ 753 | 0.167 754 | ], 755 | "y":[ 756 | 0.167 757 | ] 758 | }, 759 | "t":60, 760 | "s":[ 761 | 4 762 | ] 763 | }, 764 | { 765 | "t":80, 766 | "s":[ 767 | 3 768 | ] 769 | } 770 | ], 771 | "ix":5 772 | }, 773 | "lc":1, 774 | "lj":1, 775 | "ml":4, 776 | "bm":0, 777 | "nm":"描边 1", 778 | "mn":"ADBE Vector Graphic - Stroke", 779 | "hd":false 780 | }, 781 | { 782 | "ty":"tr", 783 | "p":{ 784 | "a":0, 785 | "k":[ 786 | 57.428, 787 | 217.928 788 | ], 789 | "ix":2 790 | }, 791 | "a":{ 792 | "a":0, 793 | "k":[ 794 | 0, 795 | 0 796 | ], 797 | "ix":1 798 | }, 799 | "s":{ 800 | "a":0, 801 | "k":[ 802 | 100, 803 | 100 804 | ], 805 | "ix":3 806 | }, 807 | "r":{ 808 | "a":0, 809 | "k":0, 810 | "ix":6 811 | }, 812 | "o":{ 813 | "a":0, 814 | "k":100, 815 | "ix":7 816 | }, 817 | "sk":{ 818 | "a":0, 819 | "k":0, 820 | "ix":4 821 | }, 822 | "sa":{ 823 | "a":0, 824 | "k":0, 825 | "ix":5 826 | }, 827 | "nm":"变换" 828 | } 829 | ], 830 | "nm":"椭圆 1", 831 | "np":3, 832 | "cix":2, 833 | "bm":0, 834 | "ix":1, 835 | "mn":"ADBE Vector Group", 836 | "hd":false 837 | } 838 | ], 839 | "ip":20, 840 | "op":1546, 841 | "st":20, 842 | "bm":0 843 | }, 844 | { 845 | "ddd":0, 846 | "ind":3, 847 | "ty":4, 848 | "nm":"1-3", 849 | "sr":1, 850 | "ks":{ 851 | "o":{ 852 | "a":1, 853 | "k":[ 854 | { 855 | "i":{ 856 | "x":[ 857 | 0.833 858 | ], 859 | "y":[ 860 | 0.833 861 | ] 862 | }, 863 | "o":{ 864 | "x":[ 865 | 0.167 866 | ], 867 | "y":[ 868 | 0.167 869 | ] 870 | }, 871 | "t":40, 872 | "s":[ 873 | 100 874 | ] 875 | }, 876 | { 877 | "t":100, 878 | "s":[ 879 | 0 880 | ] 881 | } 882 | ], 883 | "ix":11 884 | }, 885 | "r":{ 886 | "a":0, 887 | "k":0, 888 | "ix":10 889 | }, 890 | "p":{ 891 | "a":0, 892 | "k":[ 893 | 613.678, 894 | 1432.428, 895 | 0 896 | ], 897 | "ix":2 898 | }, 899 | "a":{ 900 | "a":0, 901 | "k":[ 902 | 51.178, 903 | 214.428, 904 | 0 905 | ], 906 | "ix":1 907 | }, 908 | "s":{ 909 | "a":1, 910 | "k":[ 911 | { 912 | "i":{ 913 | "x":[ 914 | 0.833, 915 | 0.833, 916 | 0.833 917 | ], 918 | "y":[ 919 | 0.833, 920 | 0.833, 921 | 0.833 922 | ] 923 | }, 924 | "o":{ 925 | "x":[ 926 | 0.167, 927 | 0.167, 928 | 0.167 929 | ], 930 | "y":[ 931 | 0.167, 932 | 0.167, 933 | 0.167 934 | ] 935 | }, 936 | "t":40, 937 | "s":[ 938 | 100, 939 | 100, 940 | 100 941 | ] 942 | }, 943 | { 944 | "i":{ 945 | "x":[ 946 | 0.833, 947 | 0.833, 948 | 0.833 949 | ], 950 | "y":[ 951 | 0.833, 952 | 0.833, 953 | 0.833 954 | ] 955 | }, 956 | "o":{ 957 | "x":[ 958 | 0.167, 959 | 0.167, 960 | 0.167 961 | ], 962 | "y":[ 963 | 0.167, 964 | 0.167, 965 | 0.167 966 | ] 967 | }, 968 | "t":60, 969 | "s":[ 970 | 150, 971 | 150, 972 | 100 973 | ] 974 | }, 975 | { 976 | "i":{ 977 | "x":[ 978 | 0.833, 979 | 0.833, 980 | 0.833 981 | ], 982 | "y":[ 983 | 0.833, 984 | 0.833, 985 | 0.833 986 | ] 987 | }, 988 | "o":{ 989 | "x":[ 990 | 0.167, 991 | 0.167, 992 | 0.167 993 | ], 994 | "y":[ 995 | 0.167, 996 | 0.167, 997 | 0.167 998 | ] 999 | }, 1000 | "t":80, 1001 | "s":[ 1002 | 200, 1003 | 200, 1004 | 100 1005 | ] 1006 | }, 1007 | { 1008 | "t":100, 1009 | "s":[ 1010 | 250, 1011 | 250, 1012 | 100 1013 | ] 1014 | } 1015 | ], 1016 | "ix":6 1017 | } 1018 | }, 1019 | "ao":0, 1020 | "shapes":[ 1021 | { 1022 | "ty":"gr", 1023 | "it":[ 1024 | { 1025 | "ind":0, 1026 | "ty":"sh", 1027 | "ix":1, 1028 | "ks":{ 1029 | "a":0, 1030 | "k":{ 1031 | "i":[ 1032 | [ 1033 | 13.767, 1034 | 0 1035 | ], 1036 | [ 1037 | 0, 1038 | -13.767 1039 | ], 1040 | [ 1041 | -13.767, 1042 | 0 1043 | ], 1044 | [ 1045 | 0, 1046 | 13.767 1047 | ] 1048 | ], 1049 | "o":[ 1050 | [ 1051 | -13.767, 1052 | 0 1053 | ], 1054 | [ 1055 | 0, 1056 | 13.767 1057 | ], 1058 | [ 1059 | 13.767, 1060 | 0 1061 | ], 1062 | [ 1063 | 0, 1064 | -13.767 1065 | ] 1066 | ], 1067 | "v":[ 1068 | [ 1069 | -6.25, 1070 | -28.428 1071 | ], 1072 | [ 1073 | -31.178, 1074 | -3.5 1075 | ], 1076 | [ 1077 | -6.25, 1078 | 21.428 1079 | ], 1080 | [ 1081 | 18.678, 1082 | -3.5 1083 | ] 1084 | ], 1085 | "c":true 1086 | }, 1087 | "ix":2 1088 | }, 1089 | "nm":"路径 1", 1090 | "mn":"ADBE Vector Shape - Group", 1091 | "hd":false 1092 | }, 1093 | { 1094 | "ty":"st", 1095 | "c":{ 1096 | "a":0, 1097 | "k":[ 1098 | 0.29411764705882354, 1099 | 0.2235294117647059, 1100 | 0.9372549019607843, 1101 | 1 1102 | ], 1103 | "ix":3 1104 | }, 1105 | "o":{ 1106 | "a":0, 1107 | "k":100, 1108 | "ix":4 1109 | }, 1110 | "w":{ 1111 | "a":1, 1112 | "k":[ 1113 | { 1114 | "i":{ 1115 | "x":[ 1116 | 0.833 1117 | ], 1118 | "y":[ 1119 | 0.833 1120 | ] 1121 | }, 1122 | "o":{ 1123 | "x":[ 1124 | 0.167 1125 | ], 1126 | "y":[ 1127 | 0.167 1128 | ] 1129 | }, 1130 | "t":40, 1131 | "s":[ 1132 | 6 1133 | ] 1134 | }, 1135 | { 1136 | "i":{ 1137 | "x":[ 1138 | 0.833 1139 | ], 1140 | "y":[ 1141 | 0.833 1142 | ] 1143 | }, 1144 | "o":{ 1145 | "x":[ 1146 | 0.167 1147 | ], 1148 | "y":[ 1149 | 0.167 1150 | ] 1151 | }, 1152 | "t":60, 1153 | "s":[ 1154 | 5 1155 | ] 1156 | }, 1157 | { 1158 | "i":{ 1159 | "x":[ 1160 | 0.833 1161 | ], 1162 | "y":[ 1163 | 0.833 1164 | ] 1165 | }, 1166 | "o":{ 1167 | "x":[ 1168 | 0.167 1169 | ], 1170 | "y":[ 1171 | 0.167 1172 | ] 1173 | }, 1174 | "t":80, 1175 | "s":[ 1176 | 4 1177 | ] 1178 | }, 1179 | { 1180 | "t":100, 1181 | "s":[ 1182 | 3 1183 | ] 1184 | } 1185 | ], 1186 | "ix":5 1187 | }, 1188 | "lc":1, 1189 | "lj":1, 1190 | "ml":4, 1191 | "bm":0, 1192 | "nm":"描边 1", 1193 | "mn":"ADBE Vector Graphic - Stroke", 1194 | "hd":false 1195 | }, 1196 | { 1197 | "ty":"tr", 1198 | "p":{ 1199 | "a":0, 1200 | "k":[ 1201 | 57.428, 1202 | 217.928 1203 | ], 1204 | "ix":2 1205 | }, 1206 | "a":{ 1207 | "a":0, 1208 | "k":[ 1209 | 0, 1210 | 0 1211 | ], 1212 | "ix":1 1213 | }, 1214 | "s":{ 1215 | "a":0, 1216 | "k":[ 1217 | 100, 1218 | 100 1219 | ], 1220 | "ix":3 1221 | }, 1222 | "r":{ 1223 | "a":0, 1224 | "k":0, 1225 | "ix":6 1226 | }, 1227 | "o":{ 1228 | "a":0, 1229 | "k":100, 1230 | "ix":7 1231 | }, 1232 | "sk":{ 1233 | "a":0, 1234 | "k":0, 1235 | "ix":4 1236 | }, 1237 | "sa":{ 1238 | "a":0, 1239 | "k":0, 1240 | "ix":5 1241 | }, 1242 | "nm":"变换" 1243 | } 1244 | ], 1245 | "nm":"椭圆 1", 1246 | "np":3, 1247 | "cix":2, 1248 | "bm":0, 1249 | "ix":1, 1250 | "mn":"ADBE Vector Group", 1251 | "hd":false 1252 | } 1253 | ], 1254 | "ip":40, 1255 | "op":1566, 1256 | "st":40, 1257 | "bm":0 1258 | }, 1259 | { 1260 | "ddd":0, 1261 | "ind":4, 1262 | "ty":4, 1263 | "nm":"1-4", 1264 | "sr":1, 1265 | "ks":{ 1266 | "o":{ 1267 | "a":1, 1268 | "k":[ 1269 | { 1270 | "i":{ 1271 | "x":[ 1272 | 0.833 1273 | ], 1274 | "y":[ 1275 | 0.833 1276 | ] 1277 | }, 1278 | "o":{ 1279 | "x":[ 1280 | 0.167 1281 | ], 1282 | "y":[ 1283 | 0.167 1284 | ] 1285 | }, 1286 | "t":-20, 1287 | "s":[ 1288 | 100 1289 | ] 1290 | }, 1291 | { 1292 | "t":40, 1293 | "s":[ 1294 | 0 1295 | ] 1296 | } 1297 | ], 1298 | "ix":11 1299 | }, 1300 | "r":{ 1301 | "a":0, 1302 | "k":0, 1303 | "ix":10 1304 | }, 1305 | "p":{ 1306 | "a":0, 1307 | "k":[ 1308 | 70.678, 1309 | 70.928, 1310 | 0 1311 | ], 1312 | "ix":2 1313 | }, 1314 | "a":{ 1315 | "a":0, 1316 | "k":[ 1317 | 51.178, 1318 | 214.428, 1319 | 0 1320 | ], 1321 | "ix":1 1322 | }, 1323 | "s":{ 1324 | "a":1, 1325 | "k":[ 1326 | { 1327 | "i":{ 1328 | "x":[ 1329 | 0.833, 1330 | 0.833, 1331 | 0.833 1332 | ], 1333 | "y":[ 1334 | 0.833, 1335 | 0.833, 1336 | 0.833 1337 | ] 1338 | }, 1339 | "o":{ 1340 | "x":[ 1341 | 0.167, 1342 | 0.167, 1343 | 0.167 1344 | ], 1345 | "y":[ 1346 | 0.167, 1347 | 0.167, 1348 | 0.167 1349 | ] 1350 | }, 1351 | "t":-20, 1352 | "s":[ 1353 | 100, 1354 | 100, 1355 | 100 1356 | ] 1357 | }, 1358 | { 1359 | "i":{ 1360 | "x":[ 1361 | 0.833, 1362 | 0.833, 1363 | 0.833 1364 | ], 1365 | "y":[ 1366 | 0.833, 1367 | 0.833, 1368 | 0.833 1369 | ] 1370 | }, 1371 | "o":{ 1372 | "x":[ 1373 | 0.167, 1374 | 0.167, 1375 | 0.167 1376 | ], 1377 | "y":[ 1378 | 0.167, 1379 | 0.167, 1380 | 0.167 1381 | ] 1382 | }, 1383 | "t":0, 1384 | "s":[ 1385 | 150, 1386 | 150, 1387 | 100 1388 | ] 1389 | }, 1390 | { 1391 | "i":{ 1392 | "x":[ 1393 | 0.833, 1394 | 0.833, 1395 | 0.833 1396 | ], 1397 | "y":[ 1398 | 0.833, 1399 | 0.833, 1400 | 0.833 1401 | ] 1402 | }, 1403 | "o":{ 1404 | "x":[ 1405 | 0.167, 1406 | 0.167, 1407 | 0.167 1408 | ], 1409 | "y":[ 1410 | 0.167, 1411 | 0.167, 1412 | 0.167 1413 | ] 1414 | }, 1415 | "t":20, 1416 | "s":[ 1417 | 200, 1418 | 200, 1419 | 100 1420 | ] 1421 | }, 1422 | { 1423 | "t":40, 1424 | "s":[ 1425 | 250, 1426 | 250, 1427 | 100 1428 | ] 1429 | } 1430 | ], 1431 | "ix":6 1432 | } 1433 | }, 1434 | "ao":0, 1435 | "shapes":[ 1436 | { 1437 | "ty":"gr", 1438 | "it":[ 1439 | { 1440 | "ind":0, 1441 | "ty":"sh", 1442 | "ix":1, 1443 | "ks":{ 1444 | "a":0, 1445 | "k":{ 1446 | "i":[ 1447 | [ 1448 | 13.767, 1449 | 0 1450 | ], 1451 | [ 1452 | 0, 1453 | -13.767 1454 | ], 1455 | [ 1456 | -13.767, 1457 | 0 1458 | ], 1459 | [ 1460 | 0, 1461 | 13.767 1462 | ] 1463 | ], 1464 | "o":[ 1465 | [ 1466 | -13.767, 1467 | 0 1468 | ], 1469 | [ 1470 | 0, 1471 | 13.767 1472 | ], 1473 | [ 1474 | 13.767, 1475 | 0 1476 | ], 1477 | [ 1478 | 0, 1479 | -13.767 1480 | ] 1481 | ], 1482 | "v":[ 1483 | [ 1484 | -6.25, 1485 | -28.428 1486 | ], 1487 | [ 1488 | -31.178, 1489 | -3.5 1490 | ], 1491 | [ 1492 | -6.25, 1493 | 21.428 1494 | ], 1495 | [ 1496 | 18.678, 1497 | -3.5 1498 | ] 1499 | ], 1500 | "c":true 1501 | }, 1502 | "ix":2 1503 | }, 1504 | "nm":"路径 1", 1505 | "mn":"ADBE Vector Shape - Group", 1506 | "hd":false 1507 | }, 1508 | { 1509 | "ty":"st", 1510 | "c":{ 1511 | "a":0, 1512 | "k":[ 1513 | 0.29411764705882354, 1514 | 0.2235294117647059, 1515 | 0.9372549019607843, 1516 | 1 1517 | ], 1518 | "ix":3 1519 | }, 1520 | "o":{ 1521 | "a":0, 1522 | "k":100, 1523 | "ix":4 1524 | }, 1525 | "w":{ 1526 | "a":1, 1527 | "k":[ 1528 | { 1529 | "i":{ 1530 | "x":[ 1531 | 0.833 1532 | ], 1533 | "y":[ 1534 | 0.833 1535 | ] 1536 | }, 1537 | "o":{ 1538 | "x":[ 1539 | 0.167 1540 | ], 1541 | "y":[ 1542 | 0.167 1543 | ] 1544 | }, 1545 | "t":-20, 1546 | "s":[ 1547 | 6 1548 | ] 1549 | }, 1550 | { 1551 | "i":{ 1552 | "x":[ 1553 | 0.833 1554 | ], 1555 | "y":[ 1556 | 0.833 1557 | ] 1558 | }, 1559 | "o":{ 1560 | "x":[ 1561 | 0.167 1562 | ], 1563 | "y":[ 1564 | 0.167 1565 | ] 1566 | }, 1567 | "t":0, 1568 | "s":[ 1569 | 5 1570 | ] 1571 | }, 1572 | { 1573 | "i":{ 1574 | "x":[ 1575 | 0.833 1576 | ], 1577 | "y":[ 1578 | 0.833 1579 | ] 1580 | }, 1581 | "o":{ 1582 | "x":[ 1583 | 0.167 1584 | ], 1585 | "y":[ 1586 | 0.167 1587 | ] 1588 | }, 1589 | "t":20, 1590 | "s":[ 1591 | 4 1592 | ] 1593 | }, 1594 | { 1595 | "t":40, 1596 | "s":[ 1597 | 3 1598 | ] 1599 | } 1600 | ], 1601 | "ix":5 1602 | }, 1603 | "lc":1, 1604 | "lj":1, 1605 | "ml":4, 1606 | "bm":0, 1607 | "nm":"描边 1", 1608 | "mn":"ADBE Vector Graphic - Stroke", 1609 | "hd":false 1610 | }, 1611 | { 1612 | "ty":"tr", 1613 | "p":{ 1614 | "a":0, 1615 | "k":[ 1616 | 57.428, 1617 | 217.928 1618 | ], 1619 | "ix":2 1620 | }, 1621 | "a":{ 1622 | "a":0, 1623 | "k":[ 1624 | 0, 1625 | 0 1626 | ], 1627 | "ix":1 1628 | }, 1629 | "s":{ 1630 | "a":0, 1631 | "k":[ 1632 | 100, 1633 | 100 1634 | ], 1635 | "ix":3 1636 | }, 1637 | "r":{ 1638 | "a":0, 1639 | "k":0, 1640 | "ix":6 1641 | }, 1642 | "o":{ 1643 | "a":0, 1644 | "k":100, 1645 | "ix":7 1646 | }, 1647 | "sk":{ 1648 | "a":0, 1649 | "k":0, 1650 | "ix":4 1651 | }, 1652 | "sa":{ 1653 | "a":0, 1654 | "k":0, 1655 | "ix":5 1656 | }, 1657 | "nm":"变换" 1658 | } 1659 | ], 1660 | "nm":"椭圆 1", 1661 | "np":3, 1662 | "cix":2, 1663 | "bm":0, 1664 | "ix":1, 1665 | "mn":"ADBE Vector Group", 1666 | "hd":false 1667 | } 1668 | ], 1669 | "ip":-20, 1670 | "op":1506, 1671 | "st":-20, 1672 | "bm":0 1673 | }, 1674 | { 1675 | "ddd":0, 1676 | "ind":5, 1677 | "ty":4, 1678 | "nm":"1-5", 1679 | "sr":1, 1680 | "ks":{ 1681 | "o":{ 1682 | "a":1, 1683 | "k":[ 1684 | { 1685 | "i":{ 1686 | "x":[ 1687 | 0.833 1688 | ], 1689 | "y":[ 1690 | 0.833 1691 | ] 1692 | }, 1693 | "o":{ 1694 | "x":[ 1695 | 0.167 1696 | ], 1697 | "y":[ 1698 | 0.167 1699 | ] 1700 | }, 1701 | "t":-40, 1702 | "s":[ 1703 | 100 1704 | ] 1705 | }, 1706 | { 1707 | "t":20, 1708 | "s":[ 1709 | 0 1710 | ] 1711 | } 1712 | ], 1713 | "ix":11 1714 | }, 1715 | "r":{ 1716 | "a":0, 1717 | "k":0, 1718 | "ix":10 1719 | }, 1720 | "p":{ 1721 | "a":0, 1722 | "k":[ 1723 | 70.678, 1724 | 70.928, 1725 | 0 1726 | ], 1727 | "ix":2 1728 | }, 1729 | "a":{ 1730 | "a":0, 1731 | "k":[ 1732 | 51.178, 1733 | 214.428, 1734 | 0 1735 | ], 1736 | "ix":1 1737 | }, 1738 | "s":{ 1739 | "a":1, 1740 | "k":[ 1741 | { 1742 | "i":{ 1743 | "x":[ 1744 | 0.833, 1745 | 0.833, 1746 | 0.833 1747 | ], 1748 | "y":[ 1749 | 0.833, 1750 | 0.833, 1751 | 0.833 1752 | ] 1753 | }, 1754 | "o":{ 1755 | "x":[ 1756 | 0.167, 1757 | 0.167, 1758 | 0.167 1759 | ], 1760 | "y":[ 1761 | 0.167, 1762 | 0.167, 1763 | 0.167 1764 | ] 1765 | }, 1766 | "t":-40, 1767 | "s":[ 1768 | 100, 1769 | 100, 1770 | 100 1771 | ] 1772 | }, 1773 | { 1774 | "i":{ 1775 | "x":[ 1776 | 0.833, 1777 | 0.833, 1778 | 0.833 1779 | ], 1780 | "y":[ 1781 | 0.833, 1782 | 0.833, 1783 | 0.833 1784 | ] 1785 | }, 1786 | "o":{ 1787 | "x":[ 1788 | 0.167, 1789 | 0.167, 1790 | 0.167 1791 | ], 1792 | "y":[ 1793 | 0.167, 1794 | 0.167, 1795 | 0.167 1796 | ] 1797 | }, 1798 | "t":-20, 1799 | "s":[ 1800 | 150, 1801 | 150, 1802 | 100 1803 | ] 1804 | }, 1805 | { 1806 | "i":{ 1807 | "x":[ 1808 | 0.833, 1809 | 0.833, 1810 | 0.833 1811 | ], 1812 | "y":[ 1813 | 0.833, 1814 | 0.833, 1815 | 0.833 1816 | ] 1817 | }, 1818 | "o":{ 1819 | "x":[ 1820 | 0.167, 1821 | 0.167, 1822 | 0.167 1823 | ], 1824 | "y":[ 1825 | 0.167, 1826 | 0.167, 1827 | 0.167 1828 | ] 1829 | }, 1830 | "t":0, 1831 | "s":[ 1832 | 200, 1833 | 200, 1834 | 100 1835 | ] 1836 | }, 1837 | { 1838 | "t":20, 1839 | "s":[ 1840 | 250, 1841 | 250, 1842 | 100 1843 | ] 1844 | } 1845 | ], 1846 | "ix":6 1847 | } 1848 | }, 1849 | "ao":0, 1850 | "shapes":[ 1851 | { 1852 | "ty":"gr", 1853 | "it":[ 1854 | { 1855 | "ind":0, 1856 | "ty":"sh", 1857 | "ix":1, 1858 | "ks":{ 1859 | "a":0, 1860 | "k":{ 1861 | "i":[ 1862 | [ 1863 | 13.767, 1864 | 0 1865 | ], 1866 | [ 1867 | 0, 1868 | -13.767 1869 | ], 1870 | [ 1871 | -13.767, 1872 | 0 1873 | ], 1874 | [ 1875 | 0, 1876 | 13.767 1877 | ] 1878 | ], 1879 | "o":[ 1880 | [ 1881 | -13.767, 1882 | 0 1883 | ], 1884 | [ 1885 | 0, 1886 | 13.767 1887 | ], 1888 | [ 1889 | 13.767, 1890 | 0 1891 | ], 1892 | [ 1893 | 0, 1894 | -13.767 1895 | ] 1896 | ], 1897 | "v":[ 1898 | [ 1899 | -6.25, 1900 | -28.428 1901 | ], 1902 | [ 1903 | -31.178, 1904 | -3.5 1905 | ], 1906 | [ 1907 | -6.25, 1908 | 21.428 1909 | ], 1910 | [ 1911 | 18.678, 1912 | -3.5 1913 | ] 1914 | ], 1915 | "c":true 1916 | }, 1917 | "ix":2 1918 | }, 1919 | "nm":"路径 1", 1920 | "mn":"ADBE Vector Shape - Group", 1921 | "hd":false 1922 | }, 1923 | { 1924 | "ty":"st", 1925 | "c":{ 1926 | "a":0, 1927 | "k":[ 1928 | 0.29411764705882354, 1929 | 0.2235294117647059, 1930 | 0.9372549019607843, 1931 | 1 1932 | ], 1933 | "ix":3 1934 | }, 1935 | "o":{ 1936 | "a":0, 1937 | "k":100, 1938 | "ix":4 1939 | }, 1940 | "w":{ 1941 | "a":1, 1942 | "k":[ 1943 | { 1944 | "i":{ 1945 | "x":[ 1946 | 0.833 1947 | ], 1948 | "y":[ 1949 | 0.833 1950 | ] 1951 | }, 1952 | "o":{ 1953 | "x":[ 1954 | 0.167 1955 | ], 1956 | "y":[ 1957 | 0.167 1958 | ] 1959 | }, 1960 | "t":-40, 1961 | "s":[ 1962 | 6 1963 | ] 1964 | }, 1965 | { 1966 | "i":{ 1967 | "x":[ 1968 | 0.833 1969 | ], 1970 | "y":[ 1971 | 0.833 1972 | ] 1973 | }, 1974 | "o":{ 1975 | "x":[ 1976 | 0.167 1977 | ], 1978 | "y":[ 1979 | 0.167 1980 | ] 1981 | }, 1982 | "t":-20, 1983 | "s":[ 1984 | 5 1985 | ] 1986 | }, 1987 | { 1988 | "i":{ 1989 | "x":[ 1990 | 0.833 1991 | ], 1992 | "y":[ 1993 | 0.833 1994 | ] 1995 | }, 1996 | "o":{ 1997 | "x":[ 1998 | 0.167 1999 | ], 2000 | "y":[ 2001 | 0.167 2002 | ] 2003 | }, 2004 | "t":0, 2005 | "s":[ 2006 | 4 2007 | ] 2008 | }, 2009 | { 2010 | "t":20, 2011 | "s":[ 2012 | 3 2013 | ] 2014 | } 2015 | ], 2016 | "ix":5 2017 | }, 2018 | "lc":1, 2019 | "lj":1, 2020 | "ml":4, 2021 | "bm":0, 2022 | "nm":"描边 1", 2023 | "mn":"ADBE Vector Graphic - Stroke", 2024 | "hd":false 2025 | }, 2026 | { 2027 | "ty":"tr", 2028 | "p":{ 2029 | "a":0, 2030 | "k":[ 2031 | 57.428, 2032 | 217.928 2033 | ], 2034 | "ix":2 2035 | }, 2036 | "a":{ 2037 | "a":0, 2038 | "k":[ 2039 | 0, 2040 | 0 2041 | ], 2042 | "ix":1 2043 | }, 2044 | "s":{ 2045 | "a":0, 2046 | "k":[ 2047 | 100, 2048 | 100 2049 | ], 2050 | "ix":3 2051 | }, 2052 | "r":{ 2053 | "a":0, 2054 | "k":0, 2055 | "ix":6 2056 | }, 2057 | "o":{ 2058 | "a":0, 2059 | "k":100, 2060 | "ix":7 2061 | }, 2062 | "sk":{ 2063 | "a":0, 2064 | "k":0, 2065 | "ix":4 2066 | }, 2067 | "sa":{ 2068 | "a":0, 2069 | "k":0, 2070 | "ix":5 2071 | }, 2072 | "nm":"变换" 2073 | } 2074 | ], 2075 | "nm":"椭圆 1", 2076 | "np":3, 2077 | "cix":2, 2078 | "bm":0, 2079 | "ix":1, 2080 | "mn":"ADBE Vector Group", 2081 | "hd":false 2082 | } 2083 | ], 2084 | "ip":-40, 2085 | "op":1486, 2086 | "st":-40, 2087 | "bm":0 2088 | } 2089 | ], 2090 | "markers":[ 2091 | 2092 | ] 2093 | } 2094 | -------------------------------------------------------------------------------- /assets/splash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JaeWangL/blabla-mobile/c25785b8472241c91896c39eced27f6fdbade9db/assets/splash.png -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = function(api) { 2 | api.cache(true); 3 | return { 4 | presets: ['babel-preset-expo'], 5 | plugins: [ 6 | [ 7 | "module-resolver", 8 | { 9 | extensions: [".ts", ".tsx", ".jsx", ".js", ".json", ".svg", ".jpg"], 10 | alias: { 11 | "@": ["./src"], 12 | "@assets": "./assets", 13 | }, 14 | }, 15 | ], 16 | 'react-native-reanimated/plugin', 17 | ], 18 | }; 19 | }; 20 | -------------------------------------------------------------------------------- /metro.config.js: -------------------------------------------------------------------------------- 1 | const { getDefaultConfig } = require("expo/metro-config"); 2 | 3 | module.exports = (() => { 4 | const config = getDefaultConfig(__dirname); 5 | 6 | const { transformer, resolver } = config; 7 | 8 | config.transformer = { 9 | ...transformer, 10 | babelTransformerPath: require.resolve("react-native-svg-transformer"), 11 | }; 12 | config.resolver = { 13 | ...resolver, 14 | assetExts: resolver.assetExts.filter((ext) => ext !== "svg"), 15 | sourceExts: [...resolver.sourceExts, "svg"], 16 | }; 17 | 18 | return config; 19 | })(); 20 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "blablamobile", 3 | "version": "1.0.0", 4 | "main": "node_modules/expo/AppEntry.js", 5 | "scripts": { 6 | "start": "expo start", 7 | "android": "expo start --android", 8 | "ios": "expo start --ios", 9 | "web": "expo start --web", 10 | "eject": "expo eject", 11 | "lint": "eslint --cache --ext .ts,.tsx,.js ./", 12 | "lint:fix": "eslint --fix --config .eslintrc.commit.js --cache --ext .ts,.tsx,.js ./", 13 | "prettify": "prettier --write ./ --ignore-path ../../.prettierignore", 14 | "pre-commit": "lint-staged" 15 | }, 16 | "dependencies": { 17 | "@expo/vector-icons": "^12.0.5", 18 | "@react-native-async-storage/async-storage": "~1.15.0", 19 | "@react-native-masked-view/masked-view": "0.2.6", 20 | "@react-navigation/bottom-tabs": "^6.2.0", 21 | "@react-navigation/native": "^6.0.8", 22 | "@react-navigation/stack": "^6.1.1", 23 | "@stomp/stompjs": "^6.1.2", 24 | "axios": "^0.26.0", 25 | "babel-plugin-module-resolver": "^4.1.0", 26 | "dayjs": "^1.10.8", 27 | "expo": "~44.0.0", 28 | "expo-app-loading": "~1.3.0", 29 | "expo-application": "~4.0.1", 30 | "expo-asset": "~8.4.6", 31 | "expo-constants": "~13.0.1", 32 | "expo-font": "~10.0.4", 33 | "expo-image-picker": "~12.0.1", 34 | "expo-localization": "~12.0.0", 35 | "expo-location": "~14.0.1", 36 | "expo-splash-screen": "~0.14.1", 37 | "expo-status-bar": "~1.2.0", 38 | "expo-task-manager": "~10.1.0", 39 | "i18n-js": "^3.8.0", 40 | "lottie-react-native": "5.0.1", 41 | "react": "17.0.1", 42 | "react-dom": "17.0.1", 43 | "react-fast-compare": "^3.2.0", 44 | "react-native": "0.64.3", 45 | "react-native-appearance": "~0.3.3", 46 | "react-native-gesture-handler": "~2.1.0", 47 | "react-native-maps": "0.29.4", 48 | "react-native-reanimated": "~2.3.1", 49 | "react-native-safe-area-context": "3.3.2", 50 | "react-native-screens": "~3.10.1", 51 | "react-native-svg": "12.1.1", 52 | "react-native-toast-message": "^2.1.1", 53 | "react-native-ui-lib": "^6.10.1", 54 | "react-native-web": "0.17.1", 55 | "react-query": "^3.34.16", 56 | "recoil": "^0.6.1", 57 | "socket.io-client": "^4.4.1", 58 | "sockjs-client": "^1.6.0", 59 | "text-encoding": "^0.7.0" 60 | }, 61 | "devDependencies": { 62 | "@babel/core": "^7.12.9", 63 | "@types/i18n-js": "^3.8.2", 64 | "@types/react": "~17.0.21", 65 | "@types/react-native": "~0.64.12", 66 | "@types/sockjs-client": "^1.5.1", 67 | "@typescript-eslint/eslint-plugin": "^5.12.1", 68 | "@typescript-eslint/parser": "^5.12.1", 69 | "eslint": "^8.9.0", 70 | "eslint-config-airbnb": "^19.0.2", 71 | "eslint-config-airbnb-typescript": "^16.1.0", 72 | "eslint-config-prettier": "^8.4.0", 73 | "eslint-import-resolver-typescript": "^2.5.0", 74 | "eslint-plugin-import": "^2.25.4", 75 | "eslint-plugin-jsx-a11y": "^6.5.1", 76 | "eslint-plugin-prettier": "^4.0.0", 77 | "eslint-plugin-react": "^7.28.0", 78 | "eslint-plugin-react-hooks": "^4.3.0", 79 | "husky": "^7.0.4", 80 | "jest": "^26.3.0", 81 | "lint-staged": "^12.3.4", 82 | "prettier": "^2.5.1", 83 | "react-native-svg-transformer": "^1.0.0", 84 | "typescript": "~4.5.5" 85 | }, 86 | "private": true 87 | } 88 | -------------------------------------------------------------------------------- /src/components/customAppbar/index.tsx: -------------------------------------------------------------------------------- 1 | import { memo, useCallback, useMemo } from 'react'; 2 | import IsEqual from 'react-fast-compare'; 3 | import { Text, TouchableOpacity, View, ViewProps } from 'react-native-ui-lib'; 4 | import { useNavigation } from '@react-navigation/native'; 5 | import IcArrowLeft from '@assets/icons/ic_arrow_left.svg'; 6 | import IcArrowLeftWhite from '@assets/icons/ic_arrow_left_white.svg'; 7 | import Divider from '../divider'; 8 | import { styles } from './styles'; 9 | 10 | type AlignmentProp = 'start' | 'center'; 11 | 12 | const getBackgroundColor = (transparent?: boolean) => { 13 | if (transparent) { 14 | return { backgroundColor: 'transparent' }; 15 | } 16 | return { backgroundColor: 'white' }; 17 | }; 18 | 19 | const getAlignmentDependentStyles = (alignment: AlignmentProp) => { 20 | if (alignment === 'center') { 21 | return { 22 | container: styles.containerCentered, 23 | titleContainer: styles.titleContainerCentered, 24 | }; 25 | } 26 | 27 | return { 28 | rightControlsContainer: styles.rightControlsContainerStart, 29 | }; 30 | }; 31 | 32 | type CustomAppBarProps = { 33 | title: string; 34 | subtitle?: string; 35 | accessoryLeft?: JSX.Element; 36 | accessoryRight?: JSX.Element; 37 | alignment?: AlignmentProp; 38 | transparent?: boolean; 39 | goBack?: boolean; 40 | } & ViewProps; 41 | 42 | function CustomAppBar(props: CustomAppBarProps): JSX.Element { 43 | const { accessoryLeft, accessoryRight, alignment, goBack, subtitle, title, transparent, style } = props; 44 | const navigation = useNavigation(); 45 | const alignmentStyles = useMemo(() => getAlignmentDependentStyles(alignment!), []); 46 | const backgroundStyles = useMemo(() => getBackgroundColor(transparent!), []); 47 | 48 | const onBackButtonPress = useCallback((): void => { 49 | if (goBack && navigation.canGoBack()) { 50 | navigation.goBack(); 51 | } 52 | }, []); 53 | 54 | const renderLeftControl = useCallback((): JSX.Element | null => { 55 | if (goBack) { 56 | return ( 57 | 58 | {transparent ? : } 59 | 60 | ); 61 | } 62 | if (accessoryLeft) { 63 | return accessoryLeft; 64 | } 65 | 66 | return null; 67 | }, []); 68 | 69 | return ( 70 | <> 71 | 72 | {renderLeftControl()} 73 | 74 | {title} 75 | {subtitle && {subtitle}} 76 | 77 | 78 | {accessoryRight && accessoryRight} 79 | 80 | 81 | {!transparent && } 82 | 83 | ); 84 | } 85 | 86 | CustomAppBar.defaultProps = { 87 | subtitle: undefined, 88 | accessoryLeft: undefined, 89 | accessoryRight: undefined, 90 | alignment: 'start', 91 | transparent: false, 92 | goBack: false, 93 | } as CustomAppBarProps; 94 | 95 | export default memo(CustomAppBar, IsEqual); 96 | -------------------------------------------------------------------------------- /src/components/customAppbar/styles.ts: -------------------------------------------------------------------------------- 1 | import { StyleSheet } from 'react-native'; 2 | import { zIndices, APPBAR_HEIGHT } from '@/themes'; 3 | 4 | export const styles = StyleSheet.create({ 5 | container: { 6 | flexDirection: 'row', 7 | alignItems: 'center', 8 | height: APPBAR_HEIGHT, 9 | zIndex: zIndices.appbar, 10 | }, 11 | containerCentered: { 12 | justifyContent: 'space-between', 13 | }, 14 | backButton: { 15 | paddingRight: 12, 16 | }, 17 | title: { 18 | fontSize: 20, 19 | fontFamily: 'PretendardBold', 20 | }, 21 | subtitle: { 22 | fontSize: 14, 23 | fontFamily: 'PretendardRegular', 24 | }, 25 | titleContainerCentered: { 26 | ...StyleSheet.absoluteFillObject, 27 | justifyContent: 'center', 28 | alignItems: 'center', 29 | }, 30 | titleContainer: { 31 | flexDirection: 'row', 32 | flex: 1, 33 | }, 34 | leftControlContainer: { 35 | flexDirection: 'row', 36 | zIndex: 1, 37 | marginLeft: 12, 38 | }, 39 | rightControlsContainer: { 40 | flexDirection: 'row', 41 | zIndex: 1, 42 | marginRight: 12, 43 | }, 44 | rightControlsContainerStart: { 45 | flex: 0, 46 | justifyContent: 'flex-end', 47 | }, 48 | }); 49 | -------------------------------------------------------------------------------- /src/components/customHeader/index.tsx: -------------------------------------------------------------------------------- 1 | import { memo } from 'react'; 2 | import IsEqual from 'react-fast-compare'; 3 | import { Text } from 'react-native-ui-lib'; 4 | import { styles } from './styles'; 5 | 6 | type CustomHeaderProps = { 7 | title: string; 8 | }; 9 | function CustomHeader(props: CustomHeaderProps): JSX.Element { 10 | const { title } = props; 11 | 12 | return {title}; 13 | } 14 | 15 | export default memo(CustomHeader, IsEqual); 16 | -------------------------------------------------------------------------------- /src/components/customHeader/styles.ts: -------------------------------------------------------------------------------- 1 | import { StyleSheet } from 'react-native'; 2 | 3 | export const styles = StyleSheet.create({ 4 | title: { 5 | fontSize: 26, 6 | fontFamily: 'PretendardBold', 7 | marginTop: 24, 8 | marginLeft: 24, 9 | marginBottom: 12, 10 | }, 11 | }); 12 | -------------------------------------------------------------------------------- /src/components/divider/index.tsx: -------------------------------------------------------------------------------- 1 | import { memo } from 'react'; 2 | import IsEqual from 'react-fast-compare'; 3 | import { View } from 'react-native-ui-lib'; 4 | 5 | type DividerProps = { 6 | color?: string; 7 | height?: number; 8 | margin?: number; 9 | }; 10 | 11 | function Divider(props: DividerProps): JSX.Element { 12 | const { color, height, margin } = props; 13 | 14 | return ; 15 | } 16 | 17 | Divider.defaultProps = { 18 | color: '#DDDDDD', 19 | height: 1, 20 | margin: 0, 21 | } as DividerProps; 22 | 23 | export default memo(Divider, IsEqual); 24 | -------------------------------------------------------------------------------- /src/components/emptyScreen/index.tsx: -------------------------------------------------------------------------------- 1 | import { memo } from 'react'; 2 | import IsEqual from 'react-fast-compare'; 3 | import { AnimatedImage, Text, View } from 'react-native-ui-lib'; 4 | import ImgEmpty from '@assets/images/img_empty.png'; 5 | import { translate } from '@/i18n'; 6 | import { styles } from './styles'; 7 | 8 | function EmptyScreen(): JSX.Element { 9 | return ( 10 | 11 | 12 | {translate('common.noData')} 13 | 14 | ); 15 | } 16 | 17 | export default memo(EmptyScreen, IsEqual); 18 | -------------------------------------------------------------------------------- /src/components/emptyScreen/styles.ts: -------------------------------------------------------------------------------- 1 | import { StyleSheet } from 'react-native'; 2 | import { defaultTheme } from '@/themes'; 3 | 4 | export const styles = StyleSheet.create({ 5 | wrapper: { 6 | flex: 1, 7 | justifyContent: 'center', 8 | alignItems: 'center', 9 | }, 10 | icon: { 11 | width: 128, 12 | height: 128, 13 | }, 14 | text: { 15 | paddingTop: 12, 16 | fontSize: 20, 17 | color: defaultTheme.captionDefault, 18 | fontFamily: 'PretendardBold', 19 | }, 20 | }); 21 | -------------------------------------------------------------------------------- /src/components/floatingButton/index.tsx: -------------------------------------------------------------------------------- 1 | import { memo } from 'react'; 2 | import IsEqual from 'react-fast-compare'; 3 | import { TouchableOpacity } from 'react-native'; 4 | import Animated from 'react-native-reanimated'; 5 | import { defaultTheme } from '@/themes'; 6 | import { styles } from './styles'; 7 | 8 | type FloatingButtonProps = { 9 | onPress: () => void; 10 | size?: number; 11 | backgroundColor?: string; 12 | offset?: number; 13 | }; 14 | 15 | function FloatingButton(props: FloatingButtonProps): JSX.Element { 16 | const { backgroundColor, offset, size, onPress } = props; 17 | 18 | return ( 19 | 30 | 41 | + 42 | 43 | 44 | ); 45 | } 46 | 47 | FloatingButton.defaultProps = { 48 | size: 60, 49 | backgroundColor: defaultTheme.primary, 50 | offset: 24, 51 | } as FloatingButtonProps; 52 | 53 | export default memo(FloatingButton, IsEqual); 54 | -------------------------------------------------------------------------------- /src/components/floatingButton/styles.ts: -------------------------------------------------------------------------------- 1 | import { StyleSheet } from 'react-native'; 2 | 3 | export const styles = StyleSheet.create({ 4 | container: { 5 | position: 'absolute', 6 | }, 7 | fabButton: { 8 | alignItems: 'center', 9 | justifyContent: 'center', 10 | }, 11 | text: { 12 | fontSize: 36, 13 | color: '#EFFBFA', 14 | }, 15 | }); 16 | -------------------------------------------------------------------------------- /src/components/keyboardAwareScrollView/index.tsx: -------------------------------------------------------------------------------- 1 | import { memo, useEffect, useState } from 'react'; 2 | import IsEqual from 'react-fast-compare'; 3 | import { Dimensions, KeyboardAvoidingView, NativeModules, Platform, ScrollView } from 'react-native'; 4 | import { styles } from './styles'; 5 | 6 | type KeyboardAwareScrollViewProps = { 7 | children: JSX.Element; 8 | useScroll?: boolean; 9 | }; 10 | 11 | const { StatusBarManager } = NativeModules; 12 | 13 | function KeyboardAwareScrollView(props: KeyboardAwareScrollViewProps): JSX.Element { 14 | const { children, useScroll } = props; 15 | const [verticalOffset, setVerticalOffset] = useState(0); 16 | 17 | useEffect(() => { 18 | if (Platform.OS === 'ios') { 19 | // @ts-ignore 20 | StatusBarManager.getHeight((statusBarFrameData) => { 21 | setVerticalOffset(statusBarFrameData.height); 22 | }); 23 | } else { 24 | setVerticalOffset(-Dimensions.get('window').height); 25 | } 26 | }, []); 27 | 28 | return ( 29 | 30 | {useScroll ? {children} : children} 31 | 32 | ); 33 | } 34 | 35 | KeyboardAwareScrollView.defaultProps = { 36 | useScroll: false, 37 | } as KeyboardAwareScrollViewProps; 38 | 39 | export default memo(KeyboardAwareScrollView, IsEqual); 40 | -------------------------------------------------------------------------------- /src/components/keyboardAwareScrollView/styles.ts: -------------------------------------------------------------------------------- 1 | import { StyleSheet } from 'react-native'; 2 | 3 | export const styles = StyleSheet.create({ 4 | wrapper: { 5 | flex: 1, 6 | backgroundColor: 'white', 7 | }, 8 | }); 9 | -------------------------------------------------------------------------------- /src/configs/api_keys.ts: -------------------------------------------------------------------------------- 1 | export const apiKeys = { 2 | chatDomain: 'https://blablachat.azurewebsites.net/', 3 | locationsDomain: 'https://blablalocations.azurewebsites.net/', 4 | locationsStomp: 'wss://blablalocations.azurewebsites.net/locations-stomp', 5 | locationsSockJS: 'https://blablalocations.azurewebsites.net/locations-stomp', 6 | postsDomain: 'https://blablaposts.azurewebsites.net/posts/', 7 | uploadDomain: 'https://blablaupload.azurewebsites.net/', 8 | }; 9 | 10 | export const queryKeys = { 11 | postsByDistance: 'postsByDistance', 12 | }; 13 | -------------------------------------------------------------------------------- /src/configs/recoil_keys.ts: -------------------------------------------------------------------------------- 1 | export enum RecoilStorageKeys { 2 | SETTINGS = 'recoil_settings', 3 | } 4 | -------------------------------------------------------------------------------- /src/configs/screen_types.ts: -------------------------------------------------------------------------------- 1 | import { PostPreviewDTO } from '../dtos/post_dtos'; 2 | 3 | export enum ScreenTypes { 4 | // UnPermissioned 5 | STACK_UN_PERMISSIONED = 'StackUnPermissioned', 6 | ON_BOARDING = 'Onboarding', 7 | 8 | // Permissioned 9 | STACK_PERMISSIONED = 'StackPermissioned', 10 | 11 | STACK_ARCHIVES = 'StackArchives', 12 | ARCHIVES = 'Archives', 13 | 14 | STACK_HOME = 'StackHome', 15 | HOME = 'Home', 16 | 17 | STACK_SETTINGS = 'StackSettings', 18 | SETTINGS = 'Settings', 19 | INFO_DEVELOPERS = 'InfoDevelopers', 20 | 21 | // Shared by tabs 22 | SHARED_POST_DETAIL = 'SharedPostDetail', 23 | SHARED_POST_WRITE = 'SharedPostWrite', 24 | SHARED_POST_CHAT = 'SharedPostChat', 25 | } 26 | 27 | export type UnPermissionedParamsList = { 28 | [ScreenTypes.ON_BOARDING]: undefined; 29 | }; 30 | 31 | export type PermissionedParamsList = { 32 | [ScreenTypes.STACK_ARCHIVES]: undefined; 33 | [ScreenTypes.STACK_HOME]: undefined; 34 | [ScreenTypes.STACK_SETTINGS]: undefined; 35 | }; 36 | 37 | export type ArchivesParamsList = { 38 | [ScreenTypes.ARCHIVES]: undefined; 39 | [ScreenTypes.SHARED_POST_DETAIL]: { post: PostPreviewDTO }; 40 | [ScreenTypes.SHARED_POST_WRITE]: undefined; 41 | [ScreenTypes.SHARED_POST_CHAT]: { post: PostPreviewDTO }; 42 | }; 43 | 44 | export type HomeParamsList = { 45 | [ScreenTypes.HOME]: undefined; 46 | [ScreenTypes.SHARED_POST_DETAIL]: { post: PostPreviewDTO }; 47 | [ScreenTypes.SHARED_POST_WRITE]: undefined; 48 | [ScreenTypes.SHARED_POST_CHAT]: { post: PostPreviewDTO }; 49 | }; 50 | 51 | export type SettingsParamsList = { 52 | [ScreenTypes.SETTINGS]: undefined; 53 | [ScreenTypes.INFO_DEVELOPERS]: undefined; 54 | }; 55 | 56 | export type RootStackParamList = { 57 | [ScreenTypes.STACK_UN_PERMISSIONED]: undefined; 58 | [ScreenTypes.STACK_PERMISSIONED]: undefined; 59 | } & UnPermissionedParamsList & 60 | PermissionedParamsList; 61 | -------------------------------------------------------------------------------- /src/configs/socket_keys.ts: -------------------------------------------------------------------------------- 1 | export enum ChatPubDestination { 2 | JOIN_ROOM = 'joinRoom', 3 | SEND_MESSAGE = 'sendMessage', 4 | } 5 | 6 | export enum ChatSubDestination { 7 | RATE_LIMITED = 'rateLimited', 8 | GET_PROFILE = 'getProfile', 9 | JOINED_NEW_MEMBER = 'joinedNewMember', 10 | LEAVED_EXISTING_MEMBER = 'leavedExistingMember', 11 | NEW_MESSAGE = 'newMessage', 12 | } 13 | 14 | export enum LocationSocketDestination { 15 | // Send 16 | UPDATE_LOCATION = '/app/location/update', 17 | 18 | // Receive Notifications 19 | CREATED_NEW_POST = '/user/queue/post/new', 20 | } 21 | -------------------------------------------------------------------------------- /src/configs/tasks_types.ts: -------------------------------------------------------------------------------- 1 | export enum TaskTypes { 2 | BACKGROUND_LOCATION = 'background-location-task', 3 | } 4 | -------------------------------------------------------------------------------- /src/dtos/chat_dtos.ts: -------------------------------------------------------------------------------- 1 | export interface JoinRoomRequest { 2 | roomId: string; 3 | 4 | /** 5 | * 1: Android 6 | * 2: iOS 7 | */ 8 | deviceType: 1 | 2; 9 | deviceId: string; 10 | } 11 | 12 | export interface SendMessageRequest { 13 | roomId: string; 14 | nickName: string; 15 | message: string; 16 | } 17 | 18 | export interface SentMessage { 19 | nickName: string; 20 | message: string; 21 | createdAt: Date; 22 | } 23 | 24 | export interface JoinedNewMember { 25 | nickName: string; 26 | joinedAt: Date; 27 | } 28 | 29 | export interface LeavedExistingMember { 30 | nickName: string; 31 | leavedAt: Date; 32 | } 33 | -------------------------------------------------------------------------------- /src/dtos/location_dtos.ts: -------------------------------------------------------------------------------- 1 | export interface UpdateLocationRequest { 2 | deviceType: 1 | 2; 3 | deviceId: string; 4 | latitude: number; 5 | longitude: number; 6 | } 7 | 8 | export interface NewPostCreatedMessage { 9 | title: string; 10 | } 11 | -------------------------------------------------------------------------------- /src/dtos/post_dtos.ts: -------------------------------------------------------------------------------- 1 | export interface CreatePostRequest { 2 | deviceType: 1 | 2; 3 | deviceId: string; 4 | latitude: number; 5 | longitude: number; 6 | country?: string; 7 | state?: string; 8 | city?: string; 9 | street?: string; 10 | detailAddress?: string; 11 | zipCode?: string; 12 | title: string; 13 | contents: string; 14 | thumbnailUrl?: string; 15 | originalFileName?: string; 16 | } 17 | 18 | export interface PostCreatedDTO { 19 | id: string; 20 | deviceType: 1 | 2; 21 | deviceId: string; 22 | } 23 | 24 | export interface PostDetailDTO { 25 | id: string; 26 | latitude: number; 27 | longitude: number; 28 | title: string; 29 | contents: string; 30 | thumbnailUrl?: string; 31 | joinedUsers: number; 32 | createdAt: Date; 33 | updatedAt: Date; 34 | } 35 | 36 | export interface PostPreviewDTO { 37 | id: string; 38 | latitude: number; 39 | longitude: number; 40 | title: string; 41 | contentsSnippet: string; 42 | thumbnailUrl?: string; 43 | distanceM: number; 44 | joinedUsers: number; 45 | createdAt: Date; 46 | updatedAt: Date; 47 | } 48 | -------------------------------------------------------------------------------- /src/dtos/socket_dtos.ts: -------------------------------------------------------------------------------- 1 | export interface RateLimited { 2 | retryRemainingMs: number; 3 | } 4 | -------------------------------------------------------------------------------- /src/dtos/upload_dtos.ts: -------------------------------------------------------------------------------- 1 | export interface UploadedThumbnailDTO { 2 | thumbnailUrl: string; 3 | origianlFileName: string; 4 | } 5 | -------------------------------------------------------------------------------- /src/helpers/device_utils.ts: -------------------------------------------------------------------------------- 1 | import * as Application from 'expo-application'; 2 | import { Platform } from 'react-native'; 3 | 4 | export interface DeviceInfo { 5 | /** 6 | * 1: Android 7 | * 2: iOS 8 | */ 9 | deviceType: 1 | 2; 10 | deviceId: string; 11 | } 12 | 13 | export async function getDeviceInfo(): Promise { 14 | let deviceId: string | null; 15 | if (Platform.OS === 'android') { 16 | deviceId = Application.androidId; 17 | } else { 18 | deviceId = await Application.getIosIdForVendorAsync(); 19 | } 20 | if (!deviceId) { 21 | return undefined; 22 | } 23 | 24 | return { 25 | deviceType: Platform.OS === 'android' ? 1 : 2, 26 | deviceId, 27 | }; 28 | } 29 | -------------------------------------------------------------------------------- /src/hooks/use_chat_socket.ts: -------------------------------------------------------------------------------- 1 | import { useCallback, useEffect, useRef, useState } from 'react'; 2 | import { Alert } from 'react-native'; 3 | import io, { Socket } from 'socket.io-client'; 4 | import { apiKeys } from '../configs/api_keys'; 5 | import { ChatPubDestination, ChatSubDestination } from '../configs/socket_keys'; 6 | import { JoinRoomRequest, JoinedNewMember, LeavedExistingMember, SentMessage } from '../dtos/chat_dtos'; 7 | import { RateLimited } from '../dtos/socket_dtos'; 8 | import { getDeviceInfo } from '../helpers/device_utils'; 9 | import { translate } from '../i18n'; 10 | 11 | type ChatSocketProps = { 12 | roomId: string; 13 | subNewMemberJoined: (res: JoinedNewMember) => void; 14 | subMemberLeaved: (res: LeavedExistingMember) => void; 15 | subNewMessage: (res: SentMessage) => void; 16 | handleDisconnect: () => void; 17 | }; 18 | 19 | type ChatSocketType = { 20 | chatSocket: Socket | null; 21 | nickName: string; 22 | }; 23 | 24 | export function useChatSocket(props: ChatSocketProps): ChatSocketType { 25 | const { handleDisconnect, roomId, subMemberLeaved, subNewMemberJoined, subNewMessage } = props; 26 | const socket = useRef(null); 27 | const [nickName, setNickName] = useState(''); 28 | 29 | const initAsync = useCallback(async (): Promise => { 30 | if (socket.current) { 31 | return; 32 | } 33 | 34 | const chatSocket = io(apiKeys.chatDomain, { 35 | reconnectionAttempts: 2, 36 | transports: ['websocket'], 37 | path: '/ws-chat/', 38 | upgrade: false, 39 | }); 40 | const deviceInfo = await getDeviceInfo(); 41 | if (!deviceInfo) { 42 | return; 43 | } 44 | 45 | chatSocket.emit(ChatPubDestination.JOIN_ROOM, { 46 | roomId, 47 | deviceType: deviceInfo.deviceType, 48 | deviceId: deviceInfo.deviceId, 49 | } as JoinRoomRequest); 50 | 51 | chatSocket.on(ChatSubDestination.GET_PROFILE, setNickName); 52 | chatSocket.on(ChatSubDestination.JOINED_NEW_MEMBER, subNewMemberJoined); 53 | chatSocket.on(ChatSubDestination.LEAVED_EXISTING_MEMBER, subMemberLeaved); 54 | chatSocket.on(ChatSubDestination.NEW_MESSAGE, subNewMessage); 55 | chatSocket.on(ChatSubDestination.RATE_LIMITED, (res: RateLimited) => { 56 | Alert.alert( 57 | translate('dialogs.rateLimitedTitle'), 58 | translate('dialogs.rateLimitedDesc', { retryRemainingS: res.retryRemainingMs / 1000 }), 59 | [{ text: translate('common.ok') }], 60 | ); 61 | }); 62 | 63 | socket.current = chatSocket; 64 | }, []); 65 | 66 | const leave = useCallback((): void => { 67 | if (handleDisconnect) { 68 | handleDisconnect(); 69 | } 70 | socket.current?.disconnect(); 71 | setNickName(''); 72 | socket.current = null; 73 | }, [socket.current]); 74 | 75 | useEffect(() => { 76 | initAsync(); 77 | 78 | return () => { 79 | leave(); 80 | }; 81 | }, []); 82 | 83 | return { chatSocket: socket.current, nickName }; 84 | } 85 | -------------------------------------------------------------------------------- /src/hooks/use_location_socket.ts: -------------------------------------------------------------------------------- 1 | import * as Location from 'expo-location'; 2 | import { useCallback, useEffect, useRef } from 'react'; 3 | import { AppState } from 'react-native'; 4 | import Toast from 'react-native-toast-message'; 5 | import { useSetRecoilState } from 'recoil'; 6 | import SockJS from 'sockjs-client'; 7 | import { Client } from '@stomp/stompjs'; 8 | import { apiKeys } from '@/configs/api_keys'; 9 | import { LocationSocketDestination } from '@/configs/socket_keys'; 10 | import { getDeviceInfo } from '@/helpers/device_utils'; 11 | import { translate } from '@/i18n'; 12 | import { locationAtom } from '@/recoils/location_states'; 13 | 14 | export function useLocationSocket(): Client | null { 15 | const setLocation = useSetRecoilState(locationAtom); 16 | const socketRef = useRef(null); 17 | const watchRef = useRef(null); 18 | 19 | const onLocationChnaged = useCallback(async (socket: Client, loc: Location.LocationObject): Promise => { 20 | const deviceInfo = await getDeviceInfo(); 21 | if (!deviceInfo) { 22 | return; 23 | } 24 | 25 | setLocation({ latitude: loc.coords.latitude, longitude: loc.coords.longitude }); 26 | 27 | if (!socket.connected) { 28 | return; 29 | } 30 | socket.publish({ 31 | destination: LocationSocketDestination.UPDATE_LOCATION, 32 | body: JSON.stringify({ 33 | deviceType: deviceInfo.deviceType, 34 | deviceId: deviceInfo.deviceId, 35 | latitude: loc.coords.latitude, 36 | longitude: loc.coords.longitude, 37 | }), 38 | }); 39 | }, []); 40 | 41 | const connectAsync = useCallback(async (): Promise => { 42 | if (socketRef.current) { 43 | return; 44 | } 45 | 46 | const locationSocket = new Client(); 47 | locationSocket.configure({ 48 | brokerURL: apiKeys.locationsStomp, 49 | reconnectDelay: 5000, 50 | heartbeatIncoming: 4000, 51 | heartbeatOutgoing: 4000, 52 | webSocketFactory() { 53 | return new SockJS(apiKeys.locationsSockJS); 54 | }, 55 | debug(str) { 56 | // TODO 57 | }, 58 | onConnect(frame) { 59 | locationSocket.subscribe(LocationSocketDestination.CREATED_NEW_POST, ({ body }: { body: string }) => { 60 | Toast.show({ 61 | type: 'success', 62 | text1: translate('dialogs.newPostTitle'), 63 | text2: body, 64 | }); 65 | }); 66 | }, 67 | onStompError(frame) { 68 | // TODO 69 | }, 70 | }); 71 | locationSocket.activate(); 72 | socketRef.current = locationSocket; 73 | 74 | const lastPosition = await Location.getLastKnownPositionAsync(); 75 | if (lastPosition) { 76 | onLocationChnaged(locationSocket, lastPosition); 77 | } 78 | }, []); 79 | 80 | const leave = useCallback((): void => { 81 | watchRef.current?.remove(); 82 | socketRef.current?.deactivate(); 83 | }, [watchRef.current, socketRef.current]); 84 | 85 | const onAppStateChange = useCallback( 86 | (nextAppState: 'active' | 'background' | 'inactive' | 'unknown' | 'extension'): void => { 87 | if (nextAppState !== 'active') { 88 | leave(); 89 | } else { 90 | connectAsync(); 91 | } 92 | }, 93 | [socketRef.current], 94 | ); 95 | 96 | useEffect(() => { 97 | connectAsync(); 98 | 99 | AppState.addEventListener('change', onAppStateChange); 100 | 101 | return () => { 102 | AppState.removeEventListener('change', onAppStateChange); 103 | leave(); 104 | }; 105 | }, []); 106 | 107 | useEffect(() => { 108 | const initAsync = async (): Promise => { 109 | if (!socketRef.current) { 110 | return; 111 | } 112 | 113 | watchRef.current = await Location.watchPositionAsync( 114 | { 115 | accuracy: Location.Accuracy.Lowest, 116 | timeInterval: 2000, 117 | distanceInterval: 5, 118 | }, 119 | (loc) => { 120 | onLocationChnaged(socketRef.current!, loc); 121 | }, 122 | ); 123 | }; 124 | 125 | initAsync(); 126 | }, [socketRef.current, socketRef.current?.connected]); 127 | 128 | return socketRef.current; 129 | } 130 | -------------------------------------------------------------------------------- /src/i18n/i18n.ts: -------------------------------------------------------------------------------- 1 | import * as Localization from 'expo-localization'; 2 | import i18n from 'i18n-js'; 3 | import en from '@assets/langs/en.json'; 4 | import ko from '@assets/langs/ko.json'; 5 | 6 | i18n.fallbacks = true; 7 | i18n.translations = { en, ko }; 8 | i18n.locale = Localization.locale || 'ko'; 9 | 10 | /** 11 | * Builds up valid keypaths for translations. 12 | * Update to your default locale of choice if not English. 13 | */ 14 | type DefaultLocale = typeof en; 15 | export type TxKeyPath = RecursiveKeyOf; 16 | 17 | type RecursiveKeyOf> = { 18 | [TKey in keyof TObj & string]: TObj[TKey] extends Record 19 | ? // @ts-ignore 20 | TKey | `${TKey}.${RecursiveKeyOf}` 21 | : TKey; 22 | }[keyof TObj & string]; 23 | -------------------------------------------------------------------------------- /src/i18n/index.ts: -------------------------------------------------------------------------------- 1 | import './i18n'; 2 | 3 | export * from './i18n'; 4 | export * from './translate'; 5 | -------------------------------------------------------------------------------- /src/i18n/translate.ts: -------------------------------------------------------------------------------- 1 | import i18n from 'i18n-js'; 2 | import { TxKeyPath } from './i18n'; 3 | 4 | /** 5 | * Translates text. 6 | * 7 | * @param key The i18n key. 8 | */ 9 | export function translate(key: TxKeyPath, options?: i18n.TranslateOptions): string { 10 | return key ? i18n.t(key, options) : ''; 11 | } 12 | -------------------------------------------------------------------------------- /src/main.tsx: -------------------------------------------------------------------------------- 1 | import AppLoading from 'expo-app-loading'; 2 | import * as Localization from 'expo-localization'; 3 | import { useAssets } from 'expo-asset'; 4 | import { useFonts } from 'expo-font'; 5 | import * as SplashScreen from 'expo-splash-screen'; 6 | import { StatusBar } from 'expo-status-bar'; 7 | import i18n from 'i18n-js'; 8 | import { useEffect, Suspense } from 'react'; 9 | import { AppearanceProvider } from 'react-native-appearance'; 10 | import { SafeAreaProvider } from 'react-native-safe-area-context'; 11 | import { QueryClient, QueryClientProvider } from 'react-query'; 12 | import { RecoilRoot } from 'recoil'; 13 | import { RootNavigator } from './navigation/root_navigator'; 14 | import './i18n'; 15 | 16 | const queryClient = new QueryClient(); 17 | 18 | SplashScreen.preventAutoHideAsync(); 19 | 20 | function App(): JSX.Element { 21 | const [fontsLoaded] = useFonts({ 22 | /* eslint-disable global-require */ 23 | PretendardBold: require('../assets/fonts/Pretendard-Bold.ttf'), 24 | PretendardExtraBold: require('../assets/fonts/Pretendard-ExtraBold.ttf'), 25 | PretendardRegular: require('../assets/fonts/Pretendard-Regular.ttf'), 26 | /* eslint-enable */ 27 | }); 28 | 29 | useEffect(() => { 30 | if (fontsLoaded) { 31 | SplashScreen.hideAsync(); 32 | } 33 | }, [fontsLoaded]); 34 | 35 | return ( 36 | 37 | }> 38 | 39 | 40 | 41 | ); 42 | } 43 | 44 | function Main(): JSX.Element { 45 | return ( 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | ); 55 | } 56 | 57 | export default Main; 58 | -------------------------------------------------------------------------------- /src/navigation/permissoned_navigator.tsx: -------------------------------------------------------------------------------- 1 | import { useCallback } from 'react'; 2 | import { createBottomTabNavigator } from '@react-navigation/bottom-tabs'; 3 | import { defaultTheme } from '@/themes'; 4 | import { PermissionedParamsList, ScreenTypes } from '@/configs/screen_types'; 5 | import IcTabArchives from '@assets/icons/ic_tab_archives.svg'; 6 | import IcTabArchivesActive from '@assets/icons/ic_tab_archives_active.svg'; 7 | import IcTabHome from '@assets/icons/ic_tab_home.svg'; 8 | import IcTabHomeActive from '@assets/icons/ic_tab_home_active.svg'; 9 | import IcTabSettings from '@assets/icons/ic_tab_settings.svg'; 10 | import IcTabSettingsActive from '@assets/icons/ic_tab_settings_active.svg'; 11 | import { useLocationSocket } from '@/hooks/use_location_socket'; 12 | import { ArchivesNavigator } from './tabs/archives_navigator'; 13 | import { HomeNavigator } from './tabs/home_navigator'; 14 | import { SettingssNavigator } from './tabs/settings_navigator'; 15 | 16 | const Tab = createBottomTabNavigator(); 17 | 18 | export function PermissionedNavigator(): JSX.Element { 19 | useLocationSocket(); 20 | 21 | const renderTabArchivesIcon = useCallback((focused: boolean, size: number): JSX.Element => { 22 | return focused ? : ; 23 | }, []); 24 | 25 | const renderTabHomeIcon = useCallback((focused: boolean, size: number): JSX.Element => { 26 | return focused ? : ; 27 | }, []); 28 | 29 | const renderTabSettingsIcon = useCallback((focused: boolean, size: number): JSX.Element => { 30 | return focused ? : ; 31 | }, []); 32 | 33 | return ( 34 | 35 | renderTabArchivesIcon(focused, 28), 43 | tabBarHideOnKeyboard: true, 44 | }} 45 | /> 46 | renderTabHomeIcon(focused, 28), 54 | tabBarHideOnKeyboard: true, 55 | }} 56 | /> 57 | renderTabSettingsIcon(focused, 28), 65 | }} 66 | /> 67 | 68 | ); 69 | } 70 | -------------------------------------------------------------------------------- /src/navigation/root_navigator.tsx: -------------------------------------------------------------------------------- 1 | import Toast from 'react-native-toast-message'; 2 | import { enableScreens } from 'react-native-screens'; 3 | import { useRecoilValue } from 'recoil'; 4 | import { NavigationContainer } from '@react-navigation/native'; 5 | import { createStackNavigator } from '@react-navigation/stack'; 6 | import { ScreenTypes, RootStackParamList } from '@/configs/screen_types'; 7 | import { settingsAtom } from '@/recoils/settings_states'; 8 | import { PermissionedNavigator } from './permissoned_navigator'; 9 | import { UnPermissionedNavigator } from './un_permissoned_navigator'; 10 | 11 | enableScreens(); 12 | 13 | const RootStack = createStackNavigator(); 14 | 15 | export function RootNavigator(): JSX.Element { 16 | const settings = useRecoilValue(settingsAtom); 17 | 18 | return ( 19 | 20 | 21 | {!settings.hasPermissions ? ( 22 | 27 | ) : ( 28 | 33 | )} 34 | 35 | 36 | 37 | ); 38 | } 39 | -------------------------------------------------------------------------------- /src/navigation/tabs/archives_navigator.tsx: -------------------------------------------------------------------------------- 1 | import { createStackNavigator } from '@react-navigation/stack'; 2 | import { ScreenTypes, ArchivesParamsList } from '@/configs/screen_types'; 3 | import ArchivesScreen from '@/screens/archives'; 4 | import PostChatScreen from '@/screens/postChat'; 5 | import PostDetailScreen from '@/screens/postDetail'; 6 | import PostWriteScreen from '@/screens/postWrite'; 7 | 8 | const Main = createStackNavigator(); 9 | 10 | export function ArchivesNavigator(): JSX.Element { 11 | return ( 12 | 13 | 14 | 15 | 16 | 17 | 18 | ); 19 | } 20 | -------------------------------------------------------------------------------- /src/navigation/tabs/home_navigator.tsx: -------------------------------------------------------------------------------- 1 | import { createStackNavigator } from '@react-navigation/stack'; 2 | import { ScreenTypes, HomeParamsList } from '@/configs/screen_types'; 3 | import HomeScreen from '@/screens/home'; 4 | import PostChatScreen from '@/screens/postChat'; 5 | import PostDetailScreen from '@/screens/postDetail'; 6 | import PostWriteScreen from '@/screens/postWrite'; 7 | 8 | const Main = createStackNavigator(); 9 | 10 | export function HomeNavigator(): JSX.Element { 11 | return ( 12 | 13 | 14 | 15 | 16 | 17 | 18 | ); 19 | } 20 | -------------------------------------------------------------------------------- /src/navigation/tabs/settings_navigator.tsx: -------------------------------------------------------------------------------- 1 | import { createStackNavigator } from '@react-navigation/stack'; 2 | import { ScreenTypes, SettingsParamsList } from '@/configs/screen_types'; 3 | import InfoDevelopersScreen from '@/screens/infoDevelopers'; 4 | import SettingsScreen from '@/screens/settings'; 5 | 6 | const Main = createStackNavigator(); 7 | 8 | export function SettingssNavigator(): JSX.Element { 9 | return ( 10 | 11 | 12 | 13 | 14 | ); 15 | } 16 | -------------------------------------------------------------------------------- /src/navigation/un_permissoned_navigator.tsx: -------------------------------------------------------------------------------- 1 | import { createStackNavigator } from '@react-navigation/stack'; 2 | import { ScreenTypes, UnPermissionedParamsList } from '@/configs/screen_types'; 3 | import OnBoardingScreen from '@/screens/onBoarding'; 4 | 5 | const Main = createStackNavigator(); 6 | 7 | export function UnPermissionedNavigator(): JSX.Element { 8 | return ( 9 | 10 | 11 | 12 | ); 13 | } 14 | -------------------------------------------------------------------------------- /src/recoils/location_states.ts: -------------------------------------------------------------------------------- 1 | import { atom } from 'recoil'; 2 | 3 | // ## -------- LocationStates -------- ## 4 | export interface LocationStates { 5 | latitude: number; 6 | longitude: number; 7 | } 8 | 9 | export const defaultLocation: LocationStates = { 10 | latitude: 0, 11 | longitude: 0, 12 | }; 13 | 14 | export const locationAtom = atom({ 15 | key: 'locationState', 16 | default: defaultLocation, 17 | }); 18 | -------------------------------------------------------------------------------- /src/recoils/settings_states.ts: -------------------------------------------------------------------------------- 1 | import { atom } from 'recoil'; 2 | import { RecoilStorageKeys } from '@/configs/recoil_keys'; 3 | import { persistAtom } from '@/utils/recoil_utils'; 4 | 5 | export interface SettingsStates { 6 | hasPermissions: boolean; 7 | } 8 | 9 | export const defaultSettings: SettingsStates = { 10 | hasPermissions: false, 11 | }; 12 | 13 | export const settingsAtom = atom({ 14 | key: 'settingsState', 15 | default: defaultSettings, 16 | effects_UNSTABLE: [persistAtom(RecoilStorageKeys.SETTINGS, defaultSettings)], 17 | }); 18 | -------------------------------------------------------------------------------- /src/screens/archives/archiveItem/index.tsx: -------------------------------------------------------------------------------- 1 | import DayJS from 'dayjs'; 2 | import { memo, useCallback } from 'react'; 3 | import IsEqual from 'react-fast-compare'; 4 | import { AnimatedImage, Text, TouchableOpacity, View } from 'react-native-ui-lib'; 5 | import { BottomTabNavigationProp } from '@react-navigation/bottom-tabs'; 6 | import { useNavigation, CompositeNavigationProp } from '@react-navigation/native'; 7 | import { StackNavigationProp } from '@react-navigation/stack'; 8 | import IcChat from '@assets/icons/ic_chat.svg'; 9 | import { ArchivesParamsList, PermissionedParamsList, ScreenTypes } from '@/configs/screen_types'; 10 | import { PostPreviewDTO } from '@/dtos/post_dtos'; 11 | import { styles } from './styles'; 12 | 13 | type ScreenNavigationProps = CompositeNavigationProp< 14 | BottomTabNavigationProp, 15 | StackNavigationProp 16 | >; 17 | 18 | type ArchiveItemProps = { 19 | item: PostPreviewDTO; 20 | }; 21 | function ArchiveItem(props: ArchiveItemProps): JSX.Element { 22 | const { item } = props; 23 | const navigation = useNavigation(); 24 | 25 | const onPress = useCallback((): void => { 26 | navigation.navigate(ScreenTypes.SHARED_POST_DETAIL, { 27 | post: item, 28 | }); 29 | }, []); 30 | 31 | return ( 32 | 33 | 34 | 35 | 42 | 43 | 44 | {item.title} 45 | 46 | 47 | {item.contentsSnippet} 48 | 49 | 50 | 51 | {item.distanceM}M 52 | {` · ${DayJS().diff(item.createdAt, 'hour')}시간 전`} 53 | 54 | 55 | 56 | {item.joinedUsers} 57 | 58 | 59 | 60 | 61 | 62 | 63 | ); 64 | } 65 | 66 | export default memo(ArchiveItem, IsEqual); 67 | -------------------------------------------------------------------------------- /src/screens/archives/archiveItem/styles.ts: -------------------------------------------------------------------------------- 1 | import { StyleSheet } from 'react-native'; 2 | import { defaultTheme } from '@/themes'; 3 | 4 | export const styles = StyleSheet.create({ 5 | wrapper: { 6 | marginHorizontal: 24, 7 | marginVertical: 8, 8 | backgroundColor: 'white', 9 | borderRadius: 12, 10 | height: 120, 11 | shadowColor: 'black', 12 | // iOS 13 | shadowOffset: { width: 0, height: 2 }, 14 | shadowOpacity: 0.12, 15 | shadowRadius: 2, 16 | 17 | // Android 18 | elevation: 2, 19 | }, 20 | container: { 21 | flex: 1, 22 | flexDirection: 'row', 23 | margin: 10, 24 | }, 25 | thumbnailImage: { 26 | width: 100, 27 | height: 100, 28 | borderRadius: 12, 29 | }, 30 | contentsContainer: { 31 | flex: 1, 32 | marginLeft: 12, 33 | justifyContent: 'space-between', 34 | }, 35 | title: { 36 | fontSize: 18, 37 | color: defaultTheme.titleDefault, 38 | fontFamily: 'PretendardBold', 39 | }, 40 | contents: { 41 | fontSize: 16, 42 | paddingTop: 4, 43 | color: defaultTheme.descDefault, 44 | fontFamily: 'PretendardRegular', 45 | }, 46 | captionContainer: { 47 | flexDirection: 'row', 48 | justifyContent: 'space-between', 49 | }, 50 | captionContentContainer: { 51 | flexDirection: 'row', 52 | }, 53 | distance: { 54 | fontSize: 14, 55 | color: defaultTheme.primary, 56 | fontFamily: 'PretendardBold', 57 | }, 58 | captionLabel: { 59 | fontSize: 14, 60 | color: defaultTheme.captionDefault, 61 | fontFamily: 'PretendardRegular', 62 | }, 63 | }); 64 | -------------------------------------------------------------------------------- /src/screens/archives/index.tsx: -------------------------------------------------------------------------------- 1 | import { memo, useCallback, useEffect } from 'react'; 2 | import IsEqual from 'react-fast-compare'; 3 | import { FlatList, Text, View } from 'react-native'; 4 | import { SafeAreaView } from 'react-native-safe-area-context'; 5 | import { useQuery } from 'react-query'; 6 | import { useRecoilValue } from 'recoil'; 7 | import { BottomTabNavigationProp } from '@react-navigation/bottom-tabs'; 8 | import { useNavigation, CompositeNavigationProp } from '@react-navigation/native'; 9 | import { StackNavigationProp } from '@react-navigation/stack'; 10 | import CustomHeader from '@/components/customHeader'; 11 | import EmptyScreen from '@/components/emptyScreen'; 12 | import FloatingButton from '@/components/floatingButton'; 13 | import { ArchivesParamsList, PermissionedParamsList, ScreenTypes } from '@/configs/screen_types'; 14 | import { queryKeys } from '@/configs/api_keys'; 15 | import { translate } from '@/i18n'; 16 | import { getPostsByDistance } from '@/services/posts_service'; 17 | import { locationAtom } from '@/recoils/location_states'; 18 | import ArchiveItem from './archiveItem'; 19 | import { styles } from './styles'; 20 | 21 | type ScreenNavigationProps = CompositeNavigationProp< 22 | BottomTabNavigationProp, 23 | StackNavigationProp 24 | >; 25 | 26 | function ArchivesScreen(): JSX.Element { 27 | const locations = useRecoilValue(locationAtom); 28 | const navigation = useNavigation(); 29 | const { 30 | isLoading, 31 | error, 32 | data: postsData, 33 | refetch, 34 | } = useQuery(queryKeys.postsByDistance, () => getPostsByDistance(locations.latitude, locations.longitude)); 35 | 36 | useEffect(() => { 37 | refetch(); 38 | }, [locations]); 39 | 40 | const onFABPress = useCallback((): void => { 41 | navigation.navigate(ScreenTypes.SHARED_POST_WRITE); 42 | }, []); 43 | 44 | const renderItem = useCallback(({ item }) => , []); 45 | 46 | if (isLoading || !postsData) { 47 | return ( 48 | 49 | Loading ... 50 | 51 | ); 52 | } 53 | if (error) { 54 | return ( 55 | 56 | Error!! 57 | 58 | ); 59 | } 60 | return ( 61 | 62 | 63 | {postsData.length < 1 ? ( 64 | 65 | ) : ( 66 | item.id} /> 67 | )} 68 | 69 | 70 | ); 71 | } 72 | 73 | export default memo(ArchivesScreen, IsEqual); 74 | -------------------------------------------------------------------------------- /src/screens/archives/styles.ts: -------------------------------------------------------------------------------- 1 | import { StyleSheet } from 'react-native'; 2 | import { defaultTheme } from '@/themes'; 3 | 4 | export const styles = StyleSheet.create({ 5 | wrapper: { 6 | flex: 1, 7 | backgroundColor: defaultTheme.backgroundDefault, 8 | }, 9 | }); 10 | -------------------------------------------------------------------------------- /src/screens/home/custom_map_styles.ts: -------------------------------------------------------------------------------- 1 | import { MapStyleElement } from 'react-native-maps'; 2 | 3 | export const customMapStyles: MapStyleElement[] = [ 4 | { 5 | elementType: 'geometry', 6 | stylers: [ 7 | { 8 | color: '#242f3e', 9 | }, 10 | ], 11 | }, 12 | { 13 | elementType: 'labels.text.fill', 14 | stylers: [ 15 | { 16 | color: '#746855', 17 | }, 18 | ], 19 | }, 20 | { 21 | elementType: 'labels.text.stroke', 22 | stylers: [ 23 | { 24 | color: '#242f3e', 25 | }, 26 | ], 27 | }, 28 | { 29 | featureType: 'administrative.locality', 30 | elementType: 'labels.text.fill', 31 | stylers: [ 32 | { 33 | color: '#d59563', 34 | }, 35 | ], 36 | }, 37 | { 38 | featureType: 'poi', 39 | elementType: 'labels.text.fill', 40 | stylers: [ 41 | { 42 | color: '#d59563', 43 | }, 44 | ], 45 | }, 46 | { 47 | featureType: 'poi.park', 48 | elementType: 'geometry', 49 | stylers: [ 50 | { 51 | color: '#263c3f', 52 | }, 53 | ], 54 | }, 55 | { 56 | featureType: 'poi.park', 57 | elementType: 'labels.text.fill', 58 | stylers: [ 59 | { 60 | color: '#6b9a76', 61 | }, 62 | ], 63 | }, 64 | { 65 | featureType: 'road', 66 | elementType: 'geometry', 67 | stylers: [ 68 | { 69 | color: '#38414e', 70 | }, 71 | ], 72 | }, 73 | { 74 | featureType: 'road', 75 | elementType: 'geometry.stroke', 76 | stylers: [ 77 | { 78 | color: '#212a37', 79 | }, 80 | ], 81 | }, 82 | { 83 | featureType: 'road', 84 | elementType: 'labels.text.fill', 85 | stylers: [ 86 | { 87 | color: '#9ca5b3', 88 | }, 89 | ], 90 | }, 91 | { 92 | featureType: 'road.highway', 93 | elementType: 'geometry', 94 | stylers: [ 95 | { 96 | color: '#746855', 97 | }, 98 | ], 99 | }, 100 | { 101 | featureType: 'road.highway', 102 | elementType: 'geometry.stroke', 103 | stylers: [ 104 | { 105 | color: '#1f2835', 106 | }, 107 | ], 108 | }, 109 | { 110 | featureType: 'road.highway', 111 | elementType: 'labels.text.fill', 112 | stylers: [ 113 | { 114 | color: '#f3d19c', 115 | }, 116 | ], 117 | }, 118 | { 119 | featureType: 'transit', 120 | elementType: 'geometry', 121 | stylers: [ 122 | { 123 | color: '#2f3948', 124 | }, 125 | ], 126 | }, 127 | { 128 | featureType: 'transit.station', 129 | elementType: 'labels.text.fill', 130 | stylers: [ 131 | { 132 | color: '#d59563', 133 | }, 134 | ], 135 | }, 136 | { 137 | featureType: 'water', 138 | elementType: 'geometry', 139 | stylers: [ 140 | { 141 | color: '#17263c', 142 | }, 143 | ], 144 | }, 145 | { 146 | featureType: 'water', 147 | elementType: 'labels.text.fill', 148 | stylers: [ 149 | { 150 | color: '#515c6d', 151 | }, 152 | ], 153 | }, 154 | { 155 | featureType: 'water', 156 | elementType: 'labels.text.stroke', 157 | stylers: [ 158 | { 159 | color: '#17263c', 160 | }, 161 | ], 162 | }, 163 | ]; 164 | -------------------------------------------------------------------------------- /src/screens/home/index.tsx: -------------------------------------------------------------------------------- 1 | import { memo, useCallback, useEffect, useMemo } from 'react'; 2 | import IsEqual from 'react-fast-compare'; 3 | import { Text } from 'react-native'; 4 | import MapView, { Marker } from 'react-native-maps'; 5 | import { View } from 'react-native-ui-lib'; 6 | import { useQuery } from 'react-query'; 7 | import { useRecoilValue } from 'recoil'; 8 | import { BottomTabNavigationProp } from '@react-navigation/bottom-tabs'; 9 | import { useNavigation, CompositeNavigationProp } from '@react-navigation/native'; 10 | import { StackNavigationProp } from '@react-navigation/stack'; 11 | import FloatingButton from '@/components/floatingButton'; 12 | import { queryKeys } from '@/configs/api_keys'; 13 | import { HomeParamsList, PermissionedParamsList, ScreenTypes } from '@/configs/screen_types'; 14 | import { PostPreviewDTO } from '@/dtos/post_dtos'; 15 | import { getPostsByDistance } from '@/services/posts_service'; 16 | import { locationAtom } from '@/recoils/location_states'; 17 | import { customMapStyles } from './custom_map_styles'; 18 | import { styles } from './styles'; 19 | 20 | function getCurrentRegion(latitude: number, longitude: number) { 21 | return { 22 | latitude, 23 | longitude, 24 | latitudeDelta: 0.0222, 25 | longitudeDelta: 0.0222, 26 | }; 27 | } 28 | 29 | type ScreenNavigationProps = CompositeNavigationProp< 30 | BottomTabNavigationProp, 31 | StackNavigationProp 32 | >; 33 | 34 | function HomeScreen(): JSX.Element { 35 | const navigation = useNavigation(); 36 | const locations = useRecoilValue(locationAtom); 37 | const { 38 | isLoading, 39 | error, 40 | data: postsData, 41 | refetch, 42 | } = useQuery(queryKeys.postsByDistance, () => getPostsByDistance(locations.latitude, locations.longitude)); 43 | 44 | useEffect(() => { 45 | refetch(); 46 | }, [locations]); 47 | 48 | const curruentRegion = useMemo( 49 | () => getCurrentRegion(locations.latitude, locations.longitude), 50 | [locations.latitude, locations.longitude], 51 | ); 52 | 53 | const getPostCoordinate = useCallback((latitude: number, longitude: number) => { 54 | return { 55 | latitude, 56 | longitude, 57 | }; 58 | }, []); 59 | 60 | const onPostMarkerPress = useCallback((item: PostPreviewDTO): void => { 61 | navigation.navigate(ScreenTypes.SHARED_POST_DETAIL, { 62 | post: item, 63 | }); 64 | }, []); 65 | 66 | const onFABPress = useCallback((): void => { 67 | navigation.navigate(ScreenTypes.SHARED_POST_WRITE); 68 | }, []); 69 | 70 | if (isLoading) { 71 | return Loading ...; 72 | } 73 | if (error) { 74 | return Error!!; 75 | } 76 | return ( 77 | 78 | 88 | {postsData?.map((post) => ( 89 | onPostMarkerPress(post)} 94 | /> 95 | ))} 96 | 97 | 98 | 99 | ); 100 | } 101 | 102 | export default memo(HomeScreen, IsEqual); 103 | -------------------------------------------------------------------------------- /src/screens/home/styles.ts: -------------------------------------------------------------------------------- 1 | import { StyleSheet } from 'react-native'; 2 | 3 | export const styles = StyleSheet.create({ 4 | wrapper: { 5 | position: 'absolute', 6 | top: 0, 7 | left: 0, 8 | right: 0, 9 | bottom: 0, 10 | justifyContent: 'flex-end', 11 | alignItems: 'center', 12 | }, 13 | mapWrapper: { 14 | position: 'absolute', 15 | top: 0, 16 | left: 0, 17 | right: 0, 18 | bottom: 0, 19 | }, 20 | }); 21 | -------------------------------------------------------------------------------- /src/screens/infoDevelopers/index.tsx: -------------------------------------------------------------------------------- 1 | import { memo } from 'react'; 2 | import IsEqual from 'react-fast-compare'; 3 | import { SafeAreaView } from 'react-native-safe-area-context'; 4 | import { Text, View } from 'react-native-ui-lib'; 5 | import CustomAppBar from '@/components/customAppbar'; 6 | import { styles } from './styles'; 7 | 8 | function InfoDevelopers(): JSX.Element { 9 | return ( 10 | 11 | 12 | 13 | 14 | {` 15 | - 이재왕 16 | - 지웅재 <93y0916@naver.com> 17 | - 이봉주 18 | `} 19 | 20 | 21 | 22 | ); 23 | } 24 | 25 | export default memo(InfoDevelopers, IsEqual); 26 | -------------------------------------------------------------------------------- /src/screens/infoDevelopers/styles.ts: -------------------------------------------------------------------------------- 1 | import { StyleSheet } from 'react-native'; 2 | import { defaultTheme } from '@/themes'; 3 | 4 | export const styles = StyleSheet.create({ 5 | wrapper: { 6 | flex: 1, 7 | backgroundColor: 'white', 8 | }, 9 | textWrapper: { 10 | padding: 24, 11 | }, 12 | text: { 13 | fontSize: 16, 14 | color: defaultTheme.titleDefault, 15 | fontFamily: 'PretendardRegular', 16 | }, 17 | }); 18 | -------------------------------------------------------------------------------- /src/screens/onBoarding/index.tsx: -------------------------------------------------------------------------------- 1 | import * as Location from 'expo-location'; 2 | import LottieView from 'lottie-react-native'; 3 | import { memo, useCallback } from 'react'; 4 | import IsEqual from 'react-fast-compare'; 5 | import { Alert } from 'react-native'; 6 | import { Button, Text, View } from 'react-native-ui-lib'; 7 | import { useRecoilState } from 'recoil'; 8 | import OnboardLottie from '@assets/lotties/onboard.json'; 9 | import { translate } from '@/i18n'; 10 | import { settingsAtom } from '@/recoils/settings_states'; 11 | import { styles } from './styles'; 12 | 13 | function OnBoardingScreen(): JSX.Element { 14 | const [settings, setSettings] = useRecoilState(settingsAtom); 15 | 16 | const onAcceptPress = useCallback(async (): Promise => { 17 | const { status } = await Location.requestForegroundPermissionsAsync(); 18 | if (status !== 'granted') { 19 | Alert.alert(translate('dialogs.permissionDeniedTitle'), translate('dialogs.permissionRequestLocation'), [ 20 | { text: translate('common.ok') }, 21 | ]); 22 | return; 23 | } 24 | 25 | setSettings({ 26 | ...settings, 27 | hasPermissions: true, 28 | }); 29 | }, []); 30 | 31 | return ( 32 | 33 | 34 | 35 | 36 | 37 | {` 38 | 왁자지껄은 위치기반 SNS 입니다 39 | 사용자의 위치정보를 알 수 있도록 위치권한을 허용해 주세요 40 | 어떠한 사용자의 데이터도 수집되지 않습니다 41 | `} 42 | 43 | 44 |