├── .babelrc ├── .buckconfig ├── .eslintignore ├── .eslintrc ├── .flowconfig ├── .gitignore ├── .watchmanconfig ├── README.md ├── android ├── app │ ├── BUCK │ ├── build.gradle │ ├── proguard-rules.pro │ └── src │ │ └── main │ │ ├── AndroidManifest.xml │ │ ├── java │ │ └── com │ │ │ └── zowninative │ │ │ ├── MainActivity.java │ │ │ └── MainApplication.java │ │ └── res │ │ ├── mipmap-hdpi │ │ └── ic_launcher.png │ │ ├── mipmap-mdpi │ │ └── ic_launcher.png │ │ ├── mipmap-xhdpi │ │ └── ic_launcher.png │ │ ├── mipmap-xxhdpi │ │ └── ic_launcher.png │ │ └── values │ │ ├── strings.xml │ │ └── styles.xml ├── build.gradle ├── gradle.properties ├── gradle │ └── wrapper │ │ ├── gradle-wrapper.jar │ │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── keystores │ ├── BUCK │ └── debug.keystore.properties └── settings.gradle ├── components ├── background.js ├── calendarIcon.js ├── city-select.js ├── cityItem.js ├── closeIcon.js ├── column.js ├── date.js ├── dateSelect.js ├── details.js ├── detailsCloudCoverage.js ├── detailsPrecip.js ├── detailsTemperature.js ├── detailsWind.js ├── dropIcon.js ├── hourScale.js ├── inAppStore.js ├── legend.js ├── locality.js ├── locationIcon.js ├── menuIcon.js ├── next.js ├── nextIcon.js ├── options.js ├── overcast.js ├── rain.js ├── reload.js ├── selectIcon.js ├── snow.js ├── t-bar.js ├── tFormat.js ├── text.js └── wind.js ├── index.android.js ├── index.ios.js ├── ios ├── zowninative.xcodeproj │ ├── project.pbxproj │ └── xcshareddata │ │ └── xcschemes │ │ └── zowninative.xcscheme ├── zowninative │ ├── AppDelegate.h │ ├── AppDelegate.m │ ├── Base.lproj │ │ └── LaunchScreen.xib │ ├── Images-2.xcassets │ │ ├── AppIcon.appiconset │ │ │ ├── Contents.json │ │ │ ├── icon@2x.png │ │ │ └── icon@3x.png │ │ ├── Brand Assets.launchimage │ │ │ ├── Contents.json │ │ │ ├── iPhone SE Lounch Screen@2x-1.png │ │ │ └── iPhone SE Lounch Screen@2x.png │ │ └── Contents.json │ ├── Images.xcassets │ │ └── AppIcon.appiconset │ │ │ ├── Contents.json │ │ │ ├── icon-60-stripes-1024-2.png │ │ │ ├── icon-60-stripes@2x.png │ │ │ └── icon-60-stripes@3x.png │ ├── Info.plist │ └── main.m └── zowninativeTests │ ├── Info.plist │ └── zowninativeTests.m ├── lib ├── beaufort.js ├── format-temperature.js ├── getDataPoints.js ├── getDimensions.js ├── getFeatureId.js ├── getFeatureLabel.js ├── reportError.js ├── setStateAnimated.js ├── showAlert.js ├── sliceDataPoints.js └── temperature-color.js ├── package.json ├── reducers ├── action-types.js └── main.js └── sketch ├── iPhone SE Lounch Screen@1x.png ├── iPhone SE Lounch Screen@2x.png ├── iPhone SE Lounch Screen@3x.png ├── icon-60-stripes-1024-2.png ├── icon-60-stripes-1024.png ├── icon-60-stripes.png ├── icon-60-stripes@17,07x.jpg ├── icon-60-stripes@17,1x.png ├── icon-60-stripes@1x.png ├── icon-60-stripes@2x.png ├── icon-60-stripes@3x.png ├── icon.sketch ├── icon@2x.png ├── icon@3x.png ├── layouts.sketch ├── locaion.svg ├── next day.svg ├── next menu level.svg └── prev day copy.svg /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["react-native"] 3 | } 4 | -------------------------------------------------------------------------------- /.buckconfig: -------------------------------------------------------------------------------- 1 | 2 | [android] 3 | target = Google Inc.:Google APIs:23 4 | 5 | [maven_repositories] 6 | central = https://repo1.maven.org/maven2 7 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | ./node_modules/ 2 | ./android/ 3 | ./ios/ 4 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "eslint:recommended", 3 | "parserOptions": { 4 | "ecmaVersion": 7 5 | }, 6 | "env": { 7 | "browser": true, 8 | "node": true, 9 | "es6": true 10 | }, 11 | "rules": { 12 | "no-console": 0 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /.flowconfig: -------------------------------------------------------------------------------- 1 | [ignore] 2 | 3 | # We fork some components by platform. 4 | .*/*[.]android.js 5 | 6 | # Ignore templates with `@flow` in header 7 | .*/local-cli/generator.* 8 | 9 | # Ignore malformed json 10 | .*/node_modules/y18n/test/.*\.json 11 | 12 | # Ignore the website subdir 13 | /website/.* 14 | 15 | # Ignore BUCK generated dirs 16 | /\.buckd/ 17 | 18 | # Ignore unexpected extra @providesModule 19 | .*/node_modules/commoner/test/source/widget/share.js 20 | 21 | # Ignore duplicate module providers 22 | # For RN Apps installed via npm, "Libraries" folder is inside node_modules/react-native but in the source repo it is in the root 23 | .*/Libraries/react-native/React.js 24 | .*/Libraries/react-native/ReactNative.js 25 | .*/node_modules/jest-runtime/build/__tests__/.* 26 | 27 | [include] 28 | 29 | [libs] 30 | node_modules/react-native/Libraries/react-native/react-native-interface.js 31 | node_modules/react-native/flow 32 | flow/ 33 | 34 | [options] 35 | module.system=haste 36 | 37 | esproposal.class_static_fields=enable 38 | esproposal.class_instance_fields=enable 39 | 40 | experimental.strict_type_args=true 41 | 42 | munge_underscores=true 43 | 44 | module.name_mapper='^image![a-zA-Z0-9$_-]+$' -> 'GlobalImageStub' 45 | module.name_mapper='^[./a-zA-Z0-9$_-]+\.\(bmp\|gif\|jpg\|jpeg\|png\|psd\|svg\|webp\|m4v\|mov\|mp4\|mpeg\|mpg\|webm\|aac\|aiff\|caf\|m4a\|mp3\|wav\|html\|pdf\)$' -> 'RelativeImageStub' 46 | 47 | suppress_type=$FlowIssue 48 | suppress_type=$FlowFixMe 49 | suppress_type=$FixMe 50 | 51 | suppress_comment=\\(.\\|\n\\)*\\$FlowFixMe\\($\\|[^(]\\|(\\(>=0\\.\\(3[0-2]\\|[1-2][0-9]\\|[0-9]\\).[0-9]\\)? *\\(site=[a-z,_]*react_native[a-z,_]*\\)?)\\) 52 | suppress_comment=\\(.\\|\n\\)*\\$FlowIssue\\((\\(>=0\\.\\(3[0-2]\\|1[0-9]\\|[1-2][0-9]\\).[0-9]\\)? *\\(site=[a-z,_]*react_native[a-z,_]*\\)?)\\)?:? #[0-9]+ 53 | suppress_comment=\\(.\\|\n\\)*\\$FlowFixedInNextDeploy 54 | 55 | unsafe.enable_getters_and_setters=true 56 | 57 | [version] 58 | ^0.32.0 59 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # OSX 2 | # 3 | .DS_Store 4 | 5 | # Xcode 6 | # 7 | build/ 8 | *.pbxuser 9 | !default.pbxuser 10 | *.mode1v3 11 | !default.mode1v3 12 | *.mode2v3 13 | !default.mode2v3 14 | *.perspectivev3 15 | !default.perspectivev3 16 | xcuserdata 17 | *.xccheckout 18 | *.moved-aside 19 | DerivedData 20 | *.hmap 21 | *.ipa 22 | *.xcuserstate 23 | project.xcworkspace 24 | 25 | # Android/IJ 26 | # 27 | *.iml 28 | .idea 29 | .gradle 30 | local.properties 31 | 32 | # node.js 33 | # 34 | node_modules/ 35 | npm-debug.log 36 | 37 | # BUCK 38 | buck-out/ 39 | \.buckd/ 40 | android/app/libs 41 | android/keystores/debug.keystore 42 | 43 | credentials.json 44 | -------------------------------------------------------------------------------- /.watchmanconfig: -------------------------------------------------------------------------------- 1 | {} -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Weather-App-React-Native 2 | 3 | iOS app for comparing weather forecast with historical data built with [react-native](https://facebook.github.io/react-native/). 4 | The source code is published for knowledge sharing purposes. 5 | 6 | # Demo 7 | 8 | 9 | 10 | 11 | 12 |
Weather-App-React-NativeGet invite on Apple TestFlight
13 | 14 | 15 | * [Veiw full screencast on YouTube](https://www.youtube.com/watch?v=Z0eKfLKoo7w) 16 | * [Download from AppStore](https://itunes.apple.com/us/app/zowni/id1140299292?ls=1&mt=8) 17 | 18 | 19 | # Used components 20 | 21 | * react-native 22 | * redux 23 | * d3.js (interpolate, scale, shape) 24 | * react-native-svg 25 | 26 | Check `package.json` for details 27 | 28 | # Used API 29 | 30 | * [DarkSky](https://darksky.net/dev/) for weather forecast and historical data 31 | * [Mapbox](https://www.mapbox.com/geocoding/) for city search 32 | 33 | # Install 34 | 35 | * clone repo 36 | * type`$ npm install` 37 | * create file `./credentials.json` 38 | 39 | ``` 40 | { 41 | "DARK_SKY_API_KEY": "{DARK_SKY_API_KEY}", 42 | "MAPBOX_API_KEY": "{MAPBOX_API_KEY}" 43 | } 44 | ``` 45 | * setup FacebookSDK as [described here](https://github.com/facebook/react-native-fbsdk) 46 | * type `$ ./node_modules/.bin/react-native link` 47 | 48 | Then you can follow [react-native docs to run on device](https://facebook.github.io/react-native/docs/running-on-device-ios.html#content). Or just [download it from AppStore](https://itunes.apple.com/us/app/zowni/id1140299292?ls=1&mt=8) and try. 49 | -------------------------------------------------------------------------------- /android/app/BUCK: -------------------------------------------------------------------------------- 1 | import re 2 | 3 | # To learn about Buck see [Docs](https://buckbuild.com/). 4 | # To run your application with Buck: 5 | # - install Buck 6 | # - `npm start` - to start the packager 7 | # - `cd android` 8 | # - `keytool -genkey -v -keystore keystores/debug.keystore -storepass android -alias androiddebugkey -keypass android -dname "CN=Android Debug,O=Android,C=US"` 9 | # - `./gradlew :app:copyDownloadableDepsToLibs` - make all Gradle compile dependencies available to Buck 10 | # - `buck install -r android/app` - compile, install and run application 11 | # 12 | 13 | lib_deps = [] 14 | for jarfile in glob(['libs/*.jar']): 15 | name = 'jars__' + re.sub(r'^.*/([^/]+)\.jar$', r'\1', jarfile) 16 | lib_deps.append(':' + name) 17 | prebuilt_jar( 18 | name = name, 19 | binary_jar = jarfile, 20 | ) 21 | 22 | for aarfile in glob(['libs/*.aar']): 23 | name = 'aars__' + re.sub(r'^.*/([^/]+)\.aar$', r'\1', aarfile) 24 | lib_deps.append(':' + name) 25 | android_prebuilt_aar( 26 | name = name, 27 | aar = aarfile, 28 | ) 29 | 30 | android_library( 31 | name = 'all-libs', 32 | exported_deps = lib_deps 33 | ) 34 | 35 | android_library( 36 | name = 'app-code', 37 | srcs = glob([ 38 | 'src/main/java/**/*.java', 39 | ]), 40 | deps = [ 41 | ':all-libs', 42 | ':build_config', 43 | ':res', 44 | ], 45 | ) 46 | 47 | android_build_config( 48 | name = 'build_config', 49 | package = 'com.zowninative', 50 | ) 51 | 52 | android_resource( 53 | name = 'res', 54 | res = 'src/main/res', 55 | package = 'com.zowninative', 56 | ) 57 | 58 | android_binary( 59 | name = 'app', 60 | package_type = 'debug', 61 | manifest = 'src/main/AndroidManifest.xml', 62 | keystore = '//android/keystores:debug', 63 | deps = [ 64 | ':app-code', 65 | ], 66 | ) 67 | -------------------------------------------------------------------------------- /android/app/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: "com.android.application" 2 | 3 | import com.android.build.OutputFile 4 | 5 | /** 6 | * The react.gradle file registers a task for each build variant (e.g. bundleDebugJsAndAssets 7 | * and bundleReleaseJsAndAssets). 8 | * These basically call `react-native bundle` with the correct arguments during the Android build 9 | * cycle. By default, bundleDebugJsAndAssets is skipped, as in debug/dev mode we prefer to load the 10 | * bundle directly from the development server. Below you can see all the possible configurations 11 | * and their defaults. If you decide to add a configuration block, make sure to add it before the 12 | * `apply from: "../../node_modules/react-native/react.gradle"` line. 13 | * 14 | * project.ext.react = [ 15 | * // the name of the generated asset file containing your JS bundle 16 | * bundleAssetName: "index.android.bundle", 17 | * 18 | * // the entry file for bundle generation 19 | * entryFile: "index.android.js", 20 | * 21 | * // whether to bundle JS and assets in debug mode 22 | * bundleInDebug: false, 23 | * 24 | * // whether to bundle JS and assets in release mode 25 | * bundleInRelease: true, 26 | * 27 | * // whether to bundle JS and assets in another build variant (if configured). 28 | * // See http://tools.android.com/tech-docs/new-build-system/user-guide#TOC-Build-Variants 29 | * // The configuration property can be in the following formats 30 | * // 'bundleIn${productFlavor}${buildType}' 31 | * // 'bundleIn${buildType}' 32 | * // bundleInFreeDebug: true, 33 | * // bundleInPaidRelease: true, 34 | * // bundleInBeta: true, 35 | * 36 | * // the root of your project, i.e. where "package.json" lives 37 | * root: "../../", 38 | * 39 | * // where to put the JS bundle asset in debug mode 40 | * jsBundleDirDebug: "$buildDir/intermediates/assets/debug", 41 | * 42 | * // where to put the JS bundle asset in release mode 43 | * jsBundleDirRelease: "$buildDir/intermediates/assets/release", 44 | * 45 | * // where to put drawable resources / React Native assets, e.g. the ones you use via 46 | * // require('./image.png')), in debug mode 47 | * resourcesDirDebug: "$buildDir/intermediates/res/merged/debug", 48 | * 49 | * // where to put drawable resources / React Native assets, e.g. the ones you use via 50 | * // require('./image.png')), in release mode 51 | * resourcesDirRelease: "$buildDir/intermediates/res/merged/release", 52 | * 53 | * // by default the gradle tasks are skipped if none of the JS files or assets change; this means 54 | * // that we don't look at files in android/ or ios/ to determine whether the tasks are up to 55 | * // date; if you have any other folders that you want to ignore for performance reasons (gradle 56 | * // indexes the entire tree), add them here. Alternatively, if you have JS files in android/ 57 | * // for example, you might want to remove it from here. 58 | * inputExcludes: ["android/**", "ios/**"], 59 | * 60 | * // override which node gets called and with what additional arguments 61 | * nodeExecutableAndArgs: ["node"] 62 | * 63 | * // supply additional arguments to the packager 64 | * extraPackagerArgs: [] 65 | * ] 66 | */ 67 | 68 | apply from: "../../node_modules/react-native/react.gradle" 69 | 70 | /** 71 | * Set this to true to create two separate APKs instead of one: 72 | * - An APK that only works on ARM devices 73 | * - An APK that only works on x86 devices 74 | * The advantage is the size of the APK is reduced by about 4MB. 75 | * Upload all the APKs to the Play Store and people will download 76 | * the correct one based on the CPU architecture of their device. 77 | */ 78 | def enableSeparateBuildPerCPUArchitecture = false 79 | 80 | /** 81 | * Run Proguard to shrink the Java bytecode in release builds. 82 | */ 83 | def enableProguardInReleaseBuilds = false 84 | 85 | android { 86 | compileSdkVersion 23 87 | buildToolsVersion "23.0.1" 88 | 89 | defaultConfig { 90 | applicationId "com.zowninative" 91 | minSdkVersion 16 92 | targetSdkVersion 22 93 | versionCode 1 94 | versionName "1.0" 95 | ndk { 96 | abiFilters "armeabi-v7a", "x86" 97 | } 98 | } 99 | splits { 100 | abi { 101 | reset() 102 | enable enableSeparateBuildPerCPUArchitecture 103 | universalApk false // If true, also generate a universal APK 104 | include "armeabi-v7a", "x86" 105 | } 106 | } 107 | buildTypes { 108 | release { 109 | minifyEnabled enableProguardInReleaseBuilds 110 | proguardFiles getDefaultProguardFile("proguard-android.txt"), "proguard-rules.pro" 111 | } 112 | } 113 | // applicationVariants are e.g. debug, release 114 | applicationVariants.all { variant -> 115 | variant.outputs.each { output -> 116 | // For each separate APK per architecture, set a unique version code as described here: 117 | // http://tools.android.com/tech-docs/new-build-system/user-guide/apk-splits 118 | def versionCodes = ["armeabi-v7a":1, "x86":2] 119 | def abi = output.getFilter(OutputFile.ABI) 120 | if (abi != null) { // null for the universal-debug, universal-release variants 121 | output.versionCodeOverride = 122 | versionCodes.get(abi) * 1048576 + defaultConfig.versionCode 123 | } 124 | } 125 | } 126 | } 127 | 128 | dependencies { 129 | compile project(':react-native-locale') 130 | compile project(':react-native-fbsdk') 131 | compile project(':react-native-svg') 132 | compile project(':react-native-blur') 133 | compile fileTree(dir: "libs", include: ["*.jar"]) 134 | compile "com.android.support:appcompat-v7:23.0.1" 135 | compile "com.facebook.react:react-native:+" // From node_modules 136 | } 137 | 138 | // Run this once to be able to run the application with BUCK 139 | // puts all compile dependencies into folder libs for BUCK to use 140 | task copyDownloadableDepsToLibs(type: Copy) { 141 | from configurations.compile 142 | into 'libs' 143 | } 144 | -------------------------------------------------------------------------------- /android/app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # By default, the flags in this file are appended to flags specified 3 | # in /usr/local/Cellar/android-sdk/24.3.3/tools/proguard/proguard-android.txt 4 | # You can edit the include path and order by changing the proguardFiles 5 | # directive in build.gradle. 6 | # 7 | # For more details, see 8 | # http://developer.android.com/guide/developing/tools/proguard.html 9 | 10 | # Add any project specific keep options here: 11 | 12 | # If your project uses WebView with JS, uncomment the following 13 | # and specify the fully qualified class name to the JavaScript interface 14 | # class: 15 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 16 | # public *; 17 | #} 18 | 19 | # Disabling obfuscation is useful if you collect stack traces from production crashes 20 | # (unless you are using a system that supports de-obfuscate the stack traces). 21 | -dontobfuscate 22 | 23 | # React Native 24 | 25 | # Keep our interfaces so they can be used by other ProGuard rules. 26 | # See http://sourceforge.net/p/proguard/bugs/466/ 27 | -keep,allowobfuscation @interface com.facebook.proguard.annotations.DoNotStrip 28 | -keep,allowobfuscation @interface com.facebook.proguard.annotations.KeepGettersAndSetters 29 | -keep,allowobfuscation @interface com.facebook.common.internal.DoNotStrip 30 | 31 | # Do not strip any method/class that is annotated with @DoNotStrip 32 | -keep @com.facebook.proguard.annotations.DoNotStrip class * 33 | -keep @com.facebook.common.internal.DoNotStrip class * 34 | -keepclassmembers class * { 35 | @com.facebook.proguard.annotations.DoNotStrip *; 36 | @com.facebook.common.internal.DoNotStrip *; 37 | } 38 | 39 | -keepclassmembers @com.facebook.proguard.annotations.KeepGettersAndSetters class * { 40 | void set*(***); 41 | *** get*(); 42 | } 43 | 44 | -keep class * extends com.facebook.react.bridge.JavaScriptModule { *; } 45 | -keep class * extends com.facebook.react.bridge.NativeModule { *; } 46 | -keepclassmembers,includedescriptorclasses class * { native ; } 47 | -keepclassmembers class * { @com.facebook.react.uimanager.UIProp ; } 48 | -keepclassmembers class * { @com.facebook.react.uimanager.annotations.ReactProp ; } 49 | -keepclassmembers class * { @com.facebook.react.uimanager.annotations.ReactPropGroup ; } 50 | 51 | -dontwarn com.facebook.react.** 52 | 53 | # okhttp 54 | 55 | -keepattributes Signature 56 | -keepattributes *Annotation* 57 | -keep class okhttp3.** { *; } 58 | -keep interface okhttp3.** { *; } 59 | -dontwarn okhttp3.** 60 | 61 | # okio 62 | 63 | -keep class sun.misc.Unsafe { *; } 64 | -dontwarn java.nio.file.* 65 | -dontwarn org.codehaus.mojo.animal_sniffer.IgnoreJRERequirement 66 | -dontwarn okio.** 67 | -------------------------------------------------------------------------------- /android/app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 5 | 6 | 7 | 8 | 9 | 12 | 13 | 18 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /android/app/src/main/java/com/zowninative/MainActivity.java: -------------------------------------------------------------------------------- 1 | package com.zowninative; 2 | 3 | import com.facebook.react.ReactActivity; 4 | import io.fixd.rctlocale.RCTLocalePackage; 5 | import com.horcrux.svg.RNSvgPackage; 6 | import com.cmcewen.blurview.BlurViewPackage; 7 | 8 | public class MainActivity extends ReactActivity { 9 | 10 | /** 11 | * Returns the name of the main component registered from JavaScript. 12 | * This is used to schedule rendering of the component. 13 | */ 14 | @Override 15 | protected String getMainComponentName() { 16 | return "zowninative"; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /android/app/src/main/java/com/zowninative/MainApplication.java: -------------------------------------------------------------------------------- 1 | package com.zowninative; 2 | 3 | import android.app.Application; 4 | import android.util.Log; 5 | 6 | import com.facebook.react.ReactApplication; 7 | import io.fixd.rctlocale.RCTLocalePackage; 8 | import com.facebook.reactnative.androidsdk.FBSDKPackage; 9 | import com.facebook.react.ReactInstanceManager; 10 | import com.facebook.react.ReactNativeHost; 11 | import com.facebook.react.ReactPackage; 12 | import com.facebook.react.shell.MainReactPackage; 13 | 14 | import java.util.Arrays; 15 | import java.util.List; 16 | 17 | public class MainApplication extends Application implements ReactApplication { 18 | 19 | private final ReactNativeHost mReactNativeHost = new ReactNativeHost(this) { 20 | @Override 21 | protected boolean getUseDeveloperSupport() { 22 | return BuildConfig.DEBUG; 23 | } 24 | 25 | @Override 26 | protected List getPackages() { 27 | return Arrays.asList( 28 | new MainReactPackage(), 29 | new RCTLocalePackage(), 30 | new FBSDKPackage() 31 | ); 32 | } 33 | }; 34 | 35 | @Override 36 | public ReactNativeHost getReactNativeHost() { 37 | return mReactNativeHost; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/delfrrr/weather-app-react-native/b0837e82ad96f76a9a52da306e4a6f9f21f32825/android/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/delfrrr/weather-app-react-native/b0837e82ad96f76a9a52da306e4a6f9f21f32825/android/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/delfrrr/weather-app-react-native/b0837e82ad96f76a9a52da306e4a6f9f21f32825/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/delfrrr/weather-app-react-native/b0837e82ad96f76a9a52da306e4a6f9f21f32825/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | zowninative 12 | 13 | -------------------------------------------------------------------------------- /android/app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /android/build.gradle: -------------------------------------------------------------------------------- 1 | // Top-level build file where you can add configuration options common to all sub-projects/modules. 2 | 3 | buildscript { 4 | repositories { 5 | jcenter() 6 | } 7 | dependencies { 8 | classpath 'com.android.tools.build:gradle:1.3.1' 9 | 10 | // NOTE: Do not place your application dependencies here; they belong 11 | // in the individual module build.gradle files 12 | } 13 | } 14 | 15 | allprojects { 16 | repositories { 17 | mavenLocal() 18 | jcenter() 19 | maven { 20 | // All of React Native (JS, Obj-C sources, Android binaries) is installed from npm 21 | url "$rootDir/../node_modules/react-native/android" 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /android/gradle.properties: -------------------------------------------------------------------------------- 1 | # Project-wide Gradle settings. 2 | 3 | # IDE (e.g. Android Studio) users: 4 | # Gradle settings configured through the IDE *will override* 5 | # any settings specified in this file. 6 | 7 | # For more details on how to configure your build environment visit 8 | # http://www.gradle.org/docs/current/userguide/build_environment.html 9 | 10 | # Specifies the JVM arguments used for the daemon process. 11 | # The setting is particularly useful for tweaking memory settings. 12 | # Default value: -Xmx10248m -XX:MaxPermSize=256m 13 | # org.gradle.jvmargs=-Xmx2048m -XX:MaxPermSize=512m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8 14 | 15 | # When configured, Gradle will run in incubating parallel mode. 16 | # This option should only be used with decoupled projects. More details, visit 17 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects 18 | # org.gradle.parallel=true 19 | 20 | android.useDeprecatedNdk=true 21 | -------------------------------------------------------------------------------- /android/gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/delfrrr/weather-app-react-native/b0837e82ad96f76a9a52da306e4a6f9f21f32825/android/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /android/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | zipStoreBase=GRADLE_USER_HOME 4 | zipStorePath=wrapper/dists 5 | distributionUrl=https\://services.gradle.org/distributions/gradle-2.4-all.zip 6 | -------------------------------------------------------------------------------- /android/gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | ############################################################################## 4 | ## 5 | ## Gradle start up script for UN*X 6 | ## 7 | ############################################################################## 8 | 9 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 10 | DEFAULT_JVM_OPTS="" 11 | 12 | APP_NAME="Gradle" 13 | APP_BASE_NAME=`basename "$0"` 14 | 15 | # Use the maximum available, or set MAX_FD != -1 to use that value. 16 | MAX_FD="maximum" 17 | 18 | warn ( ) { 19 | echo "$*" 20 | } 21 | 22 | die ( ) { 23 | echo 24 | echo "$*" 25 | echo 26 | exit 1 27 | } 28 | 29 | # OS specific support (must be 'true' or 'false'). 30 | cygwin=false 31 | msys=false 32 | darwin=false 33 | case "`uname`" in 34 | CYGWIN* ) 35 | cygwin=true 36 | ;; 37 | Darwin* ) 38 | darwin=true 39 | ;; 40 | MINGW* ) 41 | msys=true 42 | ;; 43 | esac 44 | 45 | # For Cygwin, ensure paths are in UNIX format before anything is touched. 46 | if $cygwin ; then 47 | [ -n "$JAVA_HOME" ] && JAVA_HOME=`cygpath --unix "$JAVA_HOME"` 48 | fi 49 | 50 | # Attempt to set APP_HOME 51 | # Resolve links: $0 may be a link 52 | PRG="$0" 53 | # Need this for relative symlinks. 54 | while [ -h "$PRG" ] ; do 55 | ls=`ls -ld "$PRG"` 56 | link=`expr "$ls" : '.*-> \(.*\)$'` 57 | if expr "$link" : '/.*' > /dev/null; then 58 | PRG="$link" 59 | else 60 | PRG=`dirname "$PRG"`"/$link" 61 | fi 62 | done 63 | SAVED="`pwd`" 64 | cd "`dirname \"$PRG\"`/" >&- 65 | APP_HOME="`pwd -P`" 66 | cd "$SAVED" >&- 67 | 68 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 69 | 70 | # Determine the Java command to use to start the JVM. 71 | if [ -n "$JAVA_HOME" ] ; then 72 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 73 | # IBM's JDK on AIX uses strange locations for the executables 74 | JAVACMD="$JAVA_HOME/jre/sh/java" 75 | else 76 | JAVACMD="$JAVA_HOME/bin/java" 77 | fi 78 | if [ ! -x "$JAVACMD" ] ; then 79 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 80 | 81 | Please set the JAVA_HOME variable in your environment to match the 82 | location of your Java installation." 83 | fi 84 | else 85 | JAVACMD="java" 86 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 87 | 88 | Please set the JAVA_HOME variable in your environment to match the 89 | location of your Java installation." 90 | fi 91 | 92 | # Increase the maximum file descriptors if we can. 93 | if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then 94 | MAX_FD_LIMIT=`ulimit -H -n` 95 | if [ $? -eq 0 ] ; then 96 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 97 | MAX_FD="$MAX_FD_LIMIT" 98 | fi 99 | ulimit -n $MAX_FD 100 | if [ $? -ne 0 ] ; then 101 | warn "Could not set maximum file descriptor limit: $MAX_FD" 102 | fi 103 | else 104 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 105 | fi 106 | fi 107 | 108 | # For Darwin, add options to specify how the application appears in the dock 109 | if $darwin; then 110 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 111 | fi 112 | 113 | # For Cygwin, switch paths to Windows format before running java 114 | if $cygwin ; then 115 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 116 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 117 | 118 | # We build the pattern for arguments to be converted via cygpath 119 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 120 | SEP="" 121 | for dir in $ROOTDIRSRAW ; do 122 | ROOTDIRS="$ROOTDIRS$SEP$dir" 123 | SEP="|" 124 | done 125 | OURCYGPATTERN="(^($ROOTDIRS))" 126 | # Add a user-defined pattern to the cygpath arguments 127 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 128 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 129 | fi 130 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 131 | i=0 132 | for arg in "$@" ; do 133 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 134 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 135 | 136 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 137 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 138 | else 139 | eval `echo args$i`="\"$arg\"" 140 | fi 141 | i=$((i+1)) 142 | done 143 | case $i in 144 | (0) set -- ;; 145 | (1) set -- "$args0" ;; 146 | (2) set -- "$args0" "$args1" ;; 147 | (3) set -- "$args0" "$args1" "$args2" ;; 148 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;; 149 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 150 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 151 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 152 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 153 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 154 | esac 155 | fi 156 | 157 | # Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules 158 | function splitJvmOpts() { 159 | JVM_OPTS=("$@") 160 | } 161 | eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS 162 | JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME" 163 | 164 | exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@" 165 | -------------------------------------------------------------------------------- /android/gradlew.bat: -------------------------------------------------------------------------------- 1 | @if "%DEBUG%" == "" @echo off 2 | @rem ########################################################################## 3 | @rem 4 | @rem Gradle startup script for Windows 5 | @rem 6 | @rem ########################################################################## 7 | 8 | @rem Set local scope for the variables with windows NT shell 9 | if "%OS%"=="Windows_NT" setlocal 10 | 11 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 12 | set DEFAULT_JVM_OPTS= 13 | 14 | set DIRNAME=%~dp0 15 | if "%DIRNAME%" == "" set DIRNAME=. 16 | set APP_BASE_NAME=%~n0 17 | set APP_HOME=%DIRNAME% 18 | 19 | @rem Find java.exe 20 | if defined JAVA_HOME goto findJavaFromJavaHome 21 | 22 | set JAVA_EXE=java.exe 23 | %JAVA_EXE% -version >NUL 2>&1 24 | if "%ERRORLEVEL%" == "0" goto init 25 | 26 | echo. 27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 28 | echo. 29 | echo Please set the JAVA_HOME variable in your environment to match the 30 | echo location of your Java installation. 31 | 32 | goto fail 33 | 34 | :findJavaFromJavaHome 35 | set JAVA_HOME=%JAVA_HOME:"=% 36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 37 | 38 | if exist "%JAVA_EXE%" goto init 39 | 40 | echo. 41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 42 | echo. 43 | echo Please set the JAVA_HOME variable in your environment to match the 44 | echo location of your Java installation. 45 | 46 | goto fail 47 | 48 | :init 49 | @rem Get command-line arguments, handling Windowz variants 50 | 51 | if not "%OS%" == "Windows_NT" goto win9xME_args 52 | if "%@eval[2+2]" == "4" goto 4NT_args 53 | 54 | :win9xME_args 55 | @rem Slurp the command line arguments. 56 | set CMD_LINE_ARGS= 57 | set _SKIP=2 58 | 59 | :win9xME_args_slurp 60 | if "x%~1" == "x" goto execute 61 | 62 | set CMD_LINE_ARGS=%* 63 | goto execute 64 | 65 | :4NT_args 66 | @rem Get arguments from the 4NT Shell from JP Software 67 | set CMD_LINE_ARGS=%$ 68 | 69 | :execute 70 | @rem Setup the command line 71 | 72 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 73 | 74 | @rem Execute Gradle 75 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 76 | 77 | :end 78 | @rem End local scope for the variables with windows NT shell 79 | if "%ERRORLEVEL%"=="0" goto mainEnd 80 | 81 | :fail 82 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 83 | rem the _cmd.exe /c_ return code! 84 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 85 | exit /b 1 86 | 87 | :mainEnd 88 | if "%OS%"=="Windows_NT" endlocal 89 | 90 | :omega 91 | -------------------------------------------------------------------------------- /android/keystores/BUCK: -------------------------------------------------------------------------------- 1 | keystore( 2 | name = 'debug', 3 | store = 'debug.keystore', 4 | properties = 'debug.keystore.properties', 5 | visibility = [ 6 | 'PUBLIC', 7 | ], 8 | ) 9 | -------------------------------------------------------------------------------- /android/keystores/debug.keystore.properties: -------------------------------------------------------------------------------- 1 | key.store=debug.keystore 2 | key.alias=androiddebugkey 3 | key.store.password=android 4 | key.alias.password=android 5 | -------------------------------------------------------------------------------- /android/settings.gradle: -------------------------------------------------------------------------------- 1 | rootProject.name = 'zowninative' 2 | include ':react-native-locale' 3 | project(':react-native-locale').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-locale/android') 4 | 5 | include ':app' 6 | include ':react-native-fbsdk' 7 | project(':react-native-fbsdk').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-fbsdk/android') 8 | include ':react-native-svg' 9 | project(':react-native-svg').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-svg/android') 10 | include ':react-native-blur' 11 | project(':react-native-blur').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-blur/android') 12 | -------------------------------------------------------------------------------- /components/background.js: -------------------------------------------------------------------------------- 1 | /** 2 | * background gradient 3 | * @module components/background 4 | */ 5 | 6 | const React = require('react'); 7 | const getDimensions = require('../lib/getDimensions'); 8 | const Svg = React.createFactory(require('react-native-svg').Svg); 9 | const Defs = React.createFactory(require('react-native-svg').Defs); 10 | const Stop = React.createFactory(require('react-native-svg').Stop); 11 | const connect = require('react-redux').connect; 12 | const getColorByTemp = require('../lib/temperature-color'); 13 | const getDataPoints = require('../lib/getDataPoints'); 14 | const {interpolate} = require('d3-interpolate'); 15 | const LinearGradient = React.createFactory( 16 | require('react-native-svg').LinearGradient 17 | ); 18 | const Rect = React.createFactory(require('react-native-svg').Rect); 19 | 20 | /** 21 | * @param {ForecastDataBlock[]} hourly 22 | * @param {number[]} hourRange 23 | * @param {number|null} index when need to show only one column 24 | * @returns {string[][]} colors 25 | */ 26 | function getStops(hourly, hourRange, index) { 27 | let dataBlocks = hourly; 28 | if (typeof index === 'number') { 29 | dataBlocks = [hourly[index]]; 30 | } 31 | let stops = dataBlocks.map((dataBlock) => { 32 | const points = getDataPoints(dataBlock); 33 | if (points) { 34 | let targetTempAr = points.map( 35 | p => p.apparentTemperature 36 | ); 37 | let maxTemp = Math.max(...targetTempAr.slice(...hourRange)); 38 | let minTemp = Math.min(...targetTempAr.slice(...hourRange)); 39 | return [ 40 | getColorByTemp(maxTemp), 41 | getColorByTemp(minTemp) 42 | ] 43 | } else { 44 | return [ 45 | 'rgb(84, 84, 84)', 46 | 'rgb(84, 84, 84)' 47 | ]; 48 | } 49 | }); 50 | if (stops.length < 2) { 51 | stops[1] = stops[0]; 52 | } 53 | return stops; 54 | } 55 | 56 | module.exports = connect( 57 | (state) => { 58 | return { 59 | hourRange: state.hourRange, 60 | hourly: state.hourly 61 | } 62 | } 63 | )(React.createClass({ 64 | getInitialState() { 65 | const {hourly, hourRange, index} = this.props; 66 | return { 67 | stops: getStops(hourly, hourRange, index) 68 | }; 69 | }, 70 | componentWillReceiveProps(props) { 71 | const { 72 | hourly, 73 | hourRange 74 | } = props; 75 | const index = this.props.index; 76 | let stops = getStops(hourly, hourRange, index); 77 | const interpolator = interpolate( 78 | this.state, 79 | {stops} 80 | ); 81 | const start = Date.now(); 82 | const duration = 500; 83 | this._animation = () => { 84 | const now = Date.now(); 85 | let t = (now - start) / duration; 86 | if (t > 1) { 87 | t = 1; 88 | } 89 | this.setState(interpolator(t)); 90 | if (t < 1) { 91 | requestAnimationFrame(this._animation); 92 | } 93 | } 94 | requestAnimationFrame(this._animation); 95 | }, 96 | render () { 97 | const padding = 0; 98 | let {width, viewHeight} = getDimensions(); 99 | width += 2 * padding; 100 | const {stops} = this.state; 101 | return Svg( 102 | { 103 | width, 104 | height: viewHeight, 105 | style: { 106 | position: 'absolute', 107 | left: -padding 108 | } 109 | }, 110 | Defs(null, 111 | LinearGradient( 112 | { 113 | id: 'grad1', 114 | x1: 0, 115 | y1: 0, 116 | x2: 0, 117 | y2: viewHeight 118 | }, 119 | Stop({ 120 | offset: String(0), 121 | stopColor: stops[0][0], 122 | stopOpacity: 1 123 | }), 124 | Stop({ 125 | offset: String(.5), 126 | stopColor: stops[0][1], 127 | stopOpacity: 1 128 | }) 129 | ), 130 | LinearGradient( 131 | { 132 | id: 'grad2', 133 | x1: 0, 134 | y1: 0, 135 | x2: 0, 136 | y2: viewHeight 137 | }, 138 | Stop({ 139 | offset: String(0), 140 | stopColor: stops[1][0], 141 | stopOpacity: 1 142 | }), 143 | Stop({ 144 | offset: String(.5), 145 | stopColor: stops[1][1], 146 | stopOpacity: 1 147 | }) 148 | ) 149 | ), 150 | Rect({x: 0, y: 0, width: width / 2, height: viewHeight, fill: 'url(#grad1)'}), 151 | Rect({x: width/2, y: 0, width: width / 2, height: viewHeight, fill: 'url(#grad2)'}) 152 | ); 153 | } 154 | })); 155 | -------------------------------------------------------------------------------- /components/calendarIcon.js: -------------------------------------------------------------------------------- 1 | /** 2 | * svg icon 3 | * @module components/calendar-icon 4 | */ 5 | 6 | let React = require('react'); 7 | let Svg = React.createFactory(require('react-native-svg').Svg); 8 | let Path = React.createFactory(require('react-native-svg').Path); 9 | let G = React.createFactory(require('react-native-svg').G); 10 | 11 | module.exports = React.createClass({ 12 | render: function () { 13 | return Svg( 14 | { 15 | width: 30, 16 | height: 26 17 | }, 18 | G( 19 | { 20 | fill: "none", 21 | stroke: "#FFFFFF" 22 | }, 23 | Path({ 24 | x: 1, 25 | d: 'M0,6.69262695 L2.44366686e-17,4.00862948 C1.09406692e-17,2.89929405 0.900139441,2 2.00849661,2 L3.80810547,2 L24.0008384,2 C25.1049449,2 26,2.88967395 26,3.991155 L26,22.008845 C26,23.1085295 25.1050211,24 24.0029953,24 L1.99700466,24 C0.89408944,24 0,23.0999192 0,21.9993093 L0,6.69262695 Z' 26 | }), 27 | Path({ 28 | x: 9, 29 | d: 'M1.5,4 L1.5,0' 30 | }), 31 | Path({ 32 | x: 16, 33 | d: 'M1.5,4 L1.5,0' 34 | }) 35 | ) 36 | ); 37 | } 38 | }); 39 | -------------------------------------------------------------------------------- /components/city-select.js: -------------------------------------------------------------------------------- 1 | /** 2 | * city selector 3 | * @module components/citySelect 4 | */ 5 | 6 | const React = require('react'); 7 | const {Dimensions} = require('react-native'); 8 | const text = React.createFactory(require('./text')); 9 | const textInput = React.createFactory(require('react-native').TextInput); 10 | const view = React.createFactory(require('react-native').View); 11 | const scrollView = React.createFactory(require('react-native').ScrollView); 12 | const getFeatureId = require('../lib/getFeatureId'); 13 | const getFeatureLabel = require('../lib/getFeatureLabel'); 14 | const blurView = React.createFactory( 15 | require('react-native-blur').BlurView 16 | ); 17 | const touchableOpacity = React.createFactory( 18 | require('react-native').TouchableOpacity 19 | ); 20 | const touchableHighlight = React.createFactory( 21 | require('react-native').TouchableHighlight 22 | ); 23 | const connect = require('react-redux').connect; 24 | const store = require('../reducers/main'); 25 | const cityItem = React.createFactory(require('./cityItem')); 26 | 27 | module.exports = connect( 28 | function mapStateToProps(state) { 29 | return { 30 | citySelect: state.citySelect, 31 | citySearch: state.citySearch, 32 | citySearchResult: state.citySearchResult, 33 | localities: state.localities, 34 | selectedLoacalitiesObj: state.selectedLoacalities.filter(Boolean).reduce( 35 | ( 36 | selectedLoacalitiesObj, feature 37 | ) => { 38 | selectedLoacalitiesObj[getFeatureId(feature)] = 1; 39 | return selectedLoacalitiesObj; 40 | }, {} 41 | ) 42 | }; 43 | } 44 | )(React.createClass({ 45 | getInitialState: function () { 46 | return { 47 | inputValue: '' 48 | } 49 | }, 50 | componentDidUpdate: function (prevProps) { 51 | let {inputValue} = this.state; 52 | let {citySearchResult, citySearch} = this.props; 53 | if ( 54 | citySearch && 55 | Boolean(inputValue) && 56 | citySearchResult !== prevProps.citySearchResult 57 | ) { 58 | //scroll search view to top when new results 59 | this.searchScrollViewInstance.scrollTo({x: 0, y: 0}); 60 | } 61 | }, 62 | render: function () { 63 | const {height} = Dimensions.get('window'); 64 | const { 65 | citySelect, 66 | citySearch, 67 | citySearchResult, 68 | localities, 69 | selectedLoacalitiesObj 70 | } = this.props; 71 | const {inputValue} = this.state; 72 | const top = citySelect ? 0 : -height; 73 | return blurView( 74 | { 75 | blurType: 'dark', 76 | style: { 77 | position: 'absolute', 78 | left: 0, 79 | right: 0, 80 | top, 81 | height 82 | } 83 | }, 84 | view( 85 | { 86 | style: { 87 | paddingTop: 30, 88 | paddingLeft: 10, 89 | paddingRight: 0, 90 | height: 70, 91 | flexDirection: 'row' 92 | } 93 | }, 94 | textInput( 95 | { 96 | style: { 97 | flex: 4, 98 | padding: 5, 99 | backgroundColor: 'rgba(255, 255, 255, 0.1)', 100 | borderRadius: 3, 101 | color: 'white' 102 | }, 103 | ref: 'textInput', 104 | placeholder: 'Add city', 105 | placeholderTextColor: 'rgba(255, 255, 255, 0.8)', 106 | onFocus: () => store.toggleCitySearch(), 107 | onBlur: () => store.toggleCitySearch(), 108 | onChangeText: inputValue => { 109 | store.lookupCity(inputValue); 110 | this.setState({inputValue}) 111 | }, 112 | value: inputValue || undefined 113 | } 114 | ), 115 | citySearch ? 116 | touchableOpacity( 117 | { 118 | style: { 119 | paddingLeft: 10, 120 | paddingRight: 10 121 | }, 122 | onPress: () => { 123 | this.refs.textInput.blur(); 124 | this.setState({inputValue: ''}); 125 | } 126 | }, 127 | text( 128 | { 129 | style: { 130 | lineHeight: 40, 131 | textAlign: 'center', 132 | fontSize: 18 133 | } 134 | }, 135 | 'Cancel' 136 | ) 137 | ) : 138 | touchableOpacity( 139 | { 140 | style: { 141 | paddingLeft: 10, 142 | paddingRight: 10 143 | }, 144 | onPress: () => { 145 | store.toggleCitySelect(); 146 | } 147 | }, 148 | text( 149 | { 150 | style: { 151 | lineHeight: 40, 152 | textAlign: 'center', 153 | fontSize: 18 154 | } 155 | }, 156 | 'Done' 157 | ) 158 | ) 159 | ), 160 | !citySearch && scrollView( 161 | { 162 | keyboardShouldPersistTaps: 'always' 163 | }, 164 | localities.map((feature) => { 165 | let id = getFeatureId(feature); 166 | return cityItem({ 167 | editDisabled: localities.length === 1, 168 | feature, 169 | key: id, 170 | selected: selectedLoacalitiesObj[id] 171 | }); 172 | }), 173 | view( 174 | { 175 | style: { 176 | marginTop: 10, 177 | padding: 10 178 | } 179 | }, 180 | text( 181 | { 182 | style: { 183 | textAlign: 'center' 184 | } 185 | }, 186 | 'Select one or two cities to compare' 187 | ) 188 | ) 189 | ), 190 | citySearch && view( 191 | { 192 | // behavior: 'padding', 193 | style: { 194 | flex: 1 195 | } 196 | }, 197 | citySearchResult && Boolean(inputValue) && scrollView( 198 | { 199 | ref: ref => {this.searchScrollViewInstance = ref}, 200 | keyboardShouldPersistTaps: 'always' 201 | }, 202 | citySearchResult.map((feature) => { 203 | return touchableHighlight( 204 | { 205 | key: getFeatureId(feature), 206 | style: { 207 | height: 45, 208 | paddingLeft: 15 209 | }, 210 | underlayColor: 'rgba(0, 0, 0, 0.1)', 211 | onPress: () => { 212 | this.setState({inputValue: ''}); 213 | this.refs.textInput.blur(); 214 | store.addLocality(feature); 215 | } 216 | }, 217 | view( 218 | {}, 219 | text( 220 | { 221 | style: { 222 | fontSize: 18, 223 | lineHeight: 45 224 | } 225 | }, 226 | getFeatureLabel(feature) 227 | ) 228 | ) 229 | ); 230 | }), 231 | view({height: 260}) 232 | ), 233 | !inputValue && view( 234 | { 235 | style: { 236 | padding: 10 237 | } 238 | }, 239 | text( 240 | {}, 241 | 'Search powered by Mapbox' 242 | ) 243 | ) 244 | ) 245 | ); 246 | } 247 | })); 248 | -------------------------------------------------------------------------------- /components/cityItem.js: -------------------------------------------------------------------------------- 1 | /** 2 | * city item in city select 3 | * @module components/cityItem 4 | */ 5 | 6 | 'use strict' 7 | const React = require('react'); 8 | const {Dimensions, LayoutAnimation} = require('react-native'); 9 | const text = React.createFactory(require('./text')); 10 | const selectIcon = React.createFactory(require('./selectIcon')); 11 | const view = React.createFactory(require('react-native').View); 12 | const store = require('../reducers/main'); 13 | const scrollView = React.createFactory(require('react-native').ScrollView); 14 | const getFeatureLabel = require('../lib/getFeatureLabel'); 15 | const touchableHighlight = React.createFactory( 16 | require('react-native').TouchableHighlight 17 | ); 18 | 19 | module.exports = React.createClass({ 20 | getInitialState() { 21 | return { 22 | scrollEnabled: true 23 | } 24 | }, 25 | componentDidUpdate() { 26 | const {scrollEnabled} = this.state; 27 | // const {selected} = this.props; 28 | if (!scrollEnabled) { 29 | setTimeout(() => { 30 | this.setState({ 31 | scrollEnabled: true 32 | }) 33 | }, 500); 34 | } 35 | }, 36 | render() { 37 | const {width} = Dimensions.get('window'); 38 | const {scrollEnabled} = this.state; 39 | const {feature, selected, editDisabled} = this.props; 40 | const localityAr = getFeatureLabel(feature).split(','); 41 | return view( 42 | null, 43 | scrollView( 44 | { 45 | horizontal: true, 46 | pagingEnabled: true, 47 | scrollEnabled, 48 | showsHorizontalScrollIndicator: false, 49 | ref: ref => {this.scrollViewInstance = ref}, 50 | onScroll: ({nativeEvent}) => { 51 | if (!scrollEnabled) { 52 | return; 53 | } 54 | let x = nativeEvent.contentOffset.x; 55 | if (editDisabled) { 56 | if (Math.abs(x) > 30) { 57 | this.setState({scrollEnabled: false}); 58 | this.scrollViewInstance.scrollTo({x: 0, y: 0}); 59 | } 60 | } else { 61 | if (x >= width) { 62 | LayoutAnimation.configureNext( 63 | LayoutAnimation.create(250, 'linear', 'scaleXY') 64 | ); 65 | store.removeLocality(feature); 66 | } 67 | } 68 | }, 69 | scrollEventThrottle: 50 70 | }, 71 | view( 72 | { 73 | style: { 74 | height: 50, 75 | flex: 1, 76 | width, 77 | flexDirection: 'row' 78 | } 79 | }, 80 | touchableHighlight( 81 | { 82 | style: { 83 | flex: 1 84 | }, 85 | underlayColor: 'rgba(0, 0, 0, 0.1)', 86 | onPress: () => { 87 | store.toggleSelectedLoacality(feature); 88 | } 89 | }, 90 | view( 91 | { 92 | flexDirection: 'row' 93 | }, 94 | view( 95 | { 96 | style: { 97 | position: 'relative', 98 | top: 3 99 | } 100 | }, 101 | selectIcon({selected}) 102 | ), 103 | view( 104 | { 105 | style: { 106 | flex: 1, 107 | height: 50, 108 | paddingRight: 10, 109 | flexDirection: 'column', 110 | justifyContent: 'center', 111 | borderBottomWidth: 1, 112 | borderBottomColor: 'rgba(255, 255, 255, 0.1)' 113 | } 114 | }, 115 | text( 116 | { 117 | style: { 118 | fontSize: 18 119 | }, 120 | ellipsizeMode: 'middle', 121 | numberOfLines: 1 122 | }, 123 | localityAr[0].trim() 124 | ), 125 | text( 126 | { 127 | style: { 128 | // lineHeight: 20, 129 | fontSize: 10 130 | }, 131 | ellipsizeMode: 'middle', 132 | numberOfLines: 1 133 | }, 134 | localityAr.slice(1).join(',').trim() 135 | ) 136 | ) 137 | ) 138 | ) 139 | ), 140 | view( 141 | { 142 | style: { 143 | backgroundColor: '#ff5053', 144 | height: 50, 145 | paddingLeft: 10, 146 | width//, 147 | // position: 'absolute', 148 | // left: width 149 | } 150 | }, 151 | text( 152 | { 153 | style: { 154 | lineHeight: 50, 155 | fontSize: 16 156 | } 157 | }, 158 | 'Delete' 159 | ) 160 | ) 161 | ) 162 | ); 163 | } 164 | }); 165 | -------------------------------------------------------------------------------- /components/closeIcon.js: -------------------------------------------------------------------------------- 1 | /** 2 | * close icon 3 | * @module closeIcon 4 | */ 5 | 6 | 'use strict'; 7 | 8 | let React = require('react'); 9 | let svg = React.createFactory(require('react-native-svg').Svg); 10 | let path = React.createFactory(require('react-native-svg').Path); 11 | let g = React.createFactory(require('react-native-svg').G); 12 | 13 | module.exports = React.createClass({ 14 | render() { 15 | return svg( 16 | { 17 | width: 22, 18 | height: 22 19 | }, 20 | g( 21 | { 22 | stroke: 'white', 23 | strokeWidth: 1 24 | }, 25 | path({ 26 | d: 'M2,2 L20,20' 27 | }), 28 | path({ 29 | d: 'M20,2 L2,20' 30 | }) 31 | ) 32 | ); 33 | } 34 | }); 35 | -------------------------------------------------------------------------------- /components/column.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @module components/column 3 | */ 4 | 5 | const React = require('react'); 6 | const view = React.createFactory(require('react-native').View); 7 | const touchableOpacity = React.createFactory( 8 | require('react-native').TouchableOpacity 9 | ); 10 | const hourScale = React.createFactory(require('./hourScale')); 11 | const date = React.createFactory(require('./date')); 12 | const connect = require('react-redux').connect; 13 | const {Dimensions} = require('react-native'); 14 | const store = require('../reducers/main'); 15 | const tBar = React.createFactory(require('./t-bar')); 16 | const _ = require('lodash'); 17 | 18 | 19 | 20 | module.exports = connect( 21 | function mapStateToProps(state) { 22 | return { 23 | detailsShowed: typeof state.details === 'number' 24 | } 25 | } 26 | )(React.createClass({ 27 | render: function () { 28 | const {dateOpacity, index, detailsShowed} = this.props; 29 | const {width} = Dimensions.get('window'); 30 | const barWidth = Math.ceil(width / 8); 31 | return view( 32 | { 33 | style: { 34 | flex: 1, 35 | flexDirection: 'column', 36 | flexWrap: 'nowrap', 37 | overflow: 'hidden' 38 | } 39 | }, 40 | view( 41 | { 42 | style: { 43 | flex: 1 44 | } 45 | }, 46 | date({ 47 | index, 48 | opacity: dateOpacity 49 | }) 50 | ), 51 | detailsShowed ? null : touchableOpacity( 52 | { 53 | style: { 54 | flex: 1.5, 55 | flexDirection: 'row' 56 | }, 57 | onPress: () => store.opensDetails(index) 58 | }, 59 | _.times(4).map((key) => { 60 | return tBar({ 61 | key, 62 | startHour: 24 / 4 * key, 63 | index, 64 | width: barWidth 65 | }); 66 | }) 67 | ), 68 | hourScale({hours: [3, 9, 15, 21]}) 69 | ); 70 | } 71 | })); 72 | -------------------------------------------------------------------------------- /components/date.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @module components/date 3 | */ 4 | 5 | let React = require('react'); 6 | const {AppState} = require('react-native'); 7 | let View = React.createFactory(require('react-native').View); 8 | let TouchableOpacity = React.createFactory(require('react-native').TouchableOpacity); 9 | let text = React.createFactory(require('./text')); 10 | let calendarIcon = React.createFactory(require('./calendarIcon')); 11 | let connect = require('react-redux').connect; 12 | let store = require('../reducers/main'); 13 | let moment = require('moment-timezone'); 14 | // let {scaleLinear} = require('d3-scale'); 15 | 16 | // let opacityScale = scaleLinear() 17 | // .domain([-Infinity, 0, 230]) 18 | // .range([1, 1, 0]) 19 | 20 | /** 21 | * @param {Date} date 22 | * @param {string} timezone 23 | * @returns {String} 24 | */ 25 | function formatDate (date, timezone) { 26 | const dateMoment = moment(date).tz(timezone); 27 | const currentMoment = moment(); 28 | if (dateMoment.year() === currentMoment.year()) { 29 | return dateMoment.calendar(null, { 30 | lastDay: '[Yesterday]', 31 | sameDay: '[Today]', 32 | nextDay: '[Tomorrow]', 33 | nextWeek: 'dddd', 34 | lastWeek: 'dddd', 35 | sameElse: 'MMMM' 36 | }); 37 | } else { 38 | return dateMoment.calendar(null, { 39 | lastDay: '[Yesterday]', 40 | sameDay: '[Today]', 41 | nextDay: '[Tomorrow]', 42 | nextWeek: 'MMMM YYYY', 43 | lastWeek: 'MMMM YYYY', 44 | sameElse: 'MMMM YYYY' 45 | }); 46 | } 47 | } 48 | 49 | module.exports = connect( 50 | (state) => { 51 | return { 52 | dates: state.dates, 53 | timezones: state.timezones 54 | } 55 | }, 56 | () => { 57 | return { 58 | onPress: function () { 59 | store.requestDateInput(this.props.index) 60 | } 61 | } 62 | } 63 | )(React.createClass({ 64 | componentWillMount: function () { 65 | AppState.addEventListener('change', e => { 66 | if (e === 'active') { 67 | this.forceUpdate(); 68 | } 69 | }); 70 | }, 71 | render: function () { 72 | const {timezones, index, dates, opacity} = this.props; 73 | const date = dates[index]; 74 | return TouchableOpacity( 75 | { 76 | onPress: this.props.onPress.bind(this), 77 | style: {opacity} 78 | }, 79 | View( 80 | { 81 | style: { 82 | alignItems: 'center', 83 | opacity 84 | } 85 | }, 86 | calendarIcon(), 87 | View( 88 | { 89 | left: -2, 90 | top: 5, 91 | right: 0, 92 | position: 'absolute', 93 | justifyContent: 'center', 94 | alignItems: 'center' 95 | }, 96 | text( 97 | null, 98 | moment.tz(date, timezones[index]).date() 99 | ) 100 | ), 101 | text(null, formatDate(date, timezones[index])) 102 | ) 103 | ); 104 | } 105 | })); 106 | -------------------------------------------------------------------------------- /components/dateSelect.js: -------------------------------------------------------------------------------- 1 | /** 2 | * date selector 3 | * @module components/dateSelect 4 | */ 5 | 6 | const React = require('react'); 7 | const View = React.createFactory(require('react-native').View); 8 | const text = React.createFactory(require('./text')); 9 | const moment = require('moment-timezone'); 10 | const TouchableOpacity = React.createFactory(require('react-native').TouchableOpacity); 11 | const picker = React.createFactory(require('react-native').Picker); 12 | const pickerItem = React.createFactory(require('react-native').Picker.Item); 13 | const segmentedControlIOS = React.createFactory( 14 | require('react-native').SegmentedControlIOS 15 | ); 16 | 17 | //@see https://github.com/facebook/react-native/issues/4547 18 | const DatePickerIOS = require('react-native').DatePickerIOS; 19 | DatePickerIOS.propTypes.date = React.PropTypes.any.isRequired; 20 | DatePickerIOS.propTypes.minimumDate = React.PropTypes.any.isRequired; 21 | DatePickerIOS.propTypes.maximumDate = React.PropTypes.any.isRequired; 22 | DatePickerIOS.propTypes.timeZoneOffsetInHours = React.PropTypes.any; 23 | DatePickerIOS.propTypes.onDateChange = React.PropTypes.func; 24 | 25 | const datePicker = React.createFactory(DatePickerIOS); 26 | const connect = require('react-redux').connect; 27 | const store = require('../reducers/main'); 28 | 29 | const HEIGHT = 256; 30 | const RANGE = [-30, 7]; 31 | 32 | /** 33 | * @param {Date} date 34 | * @param {string} timezone 35 | * @returns {String} 36 | */ 37 | function formatDate(date, timezone) { 38 | return moment(date).tz(timezone).calendar(null, { 39 | lastDay: '[Yesterday], D MMM', 40 | sameDay: '[Today], D MMM', 41 | nextDay: '[Tomorrow], D MMM', 42 | nextWeek: 'ddd, D MMM', 43 | lastWeek: 'ddd, D MMM', 44 | sameElse: 'ddd, D MMM' 45 | }); 46 | } 47 | 48 | module.exports = connect( 49 | function mapStateToProps(state) { 50 | return { 51 | show: state.dateSelect, 52 | dates: state.dates, 53 | timezones: state.timezones 54 | }; 55 | }, 56 | function mapDispatchToProps() { 57 | //TODO: does not make sence to keep it here 58 | //if I dont use dipspatch 59 | return { 60 | onDateChange: function (date) { 61 | let normDate = date; 62 | if (typeof(normDate) === 'number') { 63 | normDate = this.moment().add(date, 'days').toDate(); 64 | } 65 | store.setDate(this.props.show.index, normDate); 66 | }, 67 | onDone: function () { 68 | store.closeDateInput(); 69 | }, 70 | setToday: function () { 71 | store.setDate( 72 | this.props.show.index, 73 | this.moment().toDate() 74 | ); 75 | }, 76 | triggerCalendar: function () { 77 | this.setState({ 78 | datePicker: !this.state.datePicker 79 | }) 80 | } 81 | } 82 | } 83 | )(React.createClass({ 84 | getInitialState: function () { 85 | return { 86 | bottom: -HEIGHT, 87 | datePicker: false 88 | } 89 | }, 90 | componentWillReceiveProps: function (props) { 91 | let {show} = props; 92 | this.setState({bottom: show ? 0 : -HEIGHT}); 93 | }, 94 | moment: function (date) { 95 | date = date || new Date(); 96 | const {timezones, show} = this.props; 97 | const timezone = show ? timezones[show.index] : timezones[0]; 98 | return moment.tz(date, timezone); 99 | }, 100 | render: function () { 101 | const {show, timezones} = this.props; 102 | let items = []; 103 | let selectedValue = 0; 104 | let timezone = timezones[0]; 105 | if (show) { 106 | timezone = timezones[show.index]; 107 | let selectedMoment = this.moment( 108 | this.props.dates[this.props.show.index] 109 | ).startOf('day'); 110 | let currentMoment = this.moment().startOf('day'); 111 | selectedValue = Math.round( 112 | (selectedMoment - currentMoment) / 1000 / 3600/ 24 113 | ); 114 | if ( 115 | selectedValue < RANGE[0] || 116 | selectedValue > RANGE[1] 117 | ) { 118 | selectedValue = 0; 119 | } 120 | for (let i = RANGE[0]; i <= RANGE[1]; i++) { 121 | items.push(pickerItem({ 122 | key: i, 123 | value: i, 124 | label: formatDate( 125 | currentMoment.clone().add(i, 'days'), 126 | timezone 127 | ) 128 | })); 129 | } 130 | } 131 | const tz = moment.tz.zone(timezone).offset(Date.now()) / 60; 132 | return View( 133 | { 134 | style: { 135 | position: 'absolute', 136 | bottom: this.state.bottom, 137 | left: 0, 138 | right: 0, 139 | backgroundColor: 'white' 140 | } 141 | }, 142 | View( 143 | { 144 | style: { 145 | height: 40, 146 | flexDirection: 'row', 147 | justifyContent: 'space-between', 148 | alignItems: 'center' 149 | } 150 | }, 151 | TouchableOpacity( 152 | {onPress: this.props.setToday.bind(this)}, 153 | text( 154 | { 155 | style: { 156 | lineHeight: 40, 157 | fontSize: 18, 158 | paddingLeft: 10, 159 | paddingRight: 10 160 | }, 161 | class: 'link' 162 | }, 163 | 'Today' 164 | ) 165 | ), 166 | segmentedControlIOS({ 167 | style: {width: 150}, 168 | tintColor: 'rgb(17, 107, 255)', 169 | onChange: () => this.props.triggerCalendar.call(this), 170 | selectedIndex: this.state.datePicker ? 1 : 0, 171 | values: ['Recent', 'Calendar'] 172 | }), 173 | TouchableOpacity( 174 | {onPress: this.props.onDone.bind(this)}, 175 | text( 176 | { 177 | style: { 178 | lineHeight: 40, 179 | fontSize: 18, 180 | paddingLeft: 10, 181 | paddingRight: 10 182 | }, 183 | class: 'link' 184 | }, 185 | 'Done' 186 | ) 187 | ) 188 | ), 189 | this.props.show && 190 | !this.state.datePicker && 191 | picker( 192 | { 193 | style: { 194 | backgroundColor: 'rgb(243, 243, 243)' 195 | }, 196 | onValueChange: this.props.onDateChange.bind(this), 197 | selectedValue 198 | }, 199 | items 200 | ), 201 | this.props.show && 202 | this.state.datePicker && 203 | datePicker({ 204 | style: { 205 | backgroundColor: 'rgb(243, 243, 243)' 206 | }, 207 | date: this.props.dates[this.props.show.index], 208 | mode: 'date', 209 | onDateChange: this.props.onDateChange.bind(this), 210 | minimumDate: this.moment().add(-5, 'years').toDate(), 211 | maximumDate: this.moment().add(7, 'days').toDate(), 212 | timeZoneOffsetInHours: -tz 213 | }) 214 | ); 215 | } 216 | })); 217 | -------------------------------------------------------------------------------- /components/details.js: -------------------------------------------------------------------------------- 1 | /** 2 | * weather details for day 3 | * @module components/details 4 | */ 5 | 6 | const React = require('react'); 7 | const closeIcon = React.createFactory(require('./closeIcon')); 8 | const date = React.createFactory(require('./date')); 9 | const view = React.createFactory(require('react-native').View); 10 | const {width, height, statusBarHeight} = require('../lib/getDimensions')(); 11 | const touchableOpacity = React.createFactory( 12 | require('react-native').TouchableOpacity 13 | ); 14 | const locality = React.createFactory(require('./locality')); 15 | const detailsTemperature = React.createFactory(require('./detailsTemperature')); 16 | const detailsWind = React.createFactory(require('./detailsWind')); 17 | const detailsPrecip = React.createFactory(require('./detailsPrecip')); 18 | const detailsCloudCoverage = React.createFactory(require('./detailsCloudCoverage')); 19 | const hourScale = React.createFactory(require('./hourScale')); 20 | const connect = require('react-redux').connect; 21 | const store = require('../reducers/main'); 22 | const background = React.createFactory(require('./background')); 23 | const sliceDataPoints = require('../lib/sliceDataPoints'); 24 | 25 | module.exports = connect( 26 | function mapStateToProps(state) { 27 | let index = state.details; 28 | let dataPoints = []; 29 | if (typeof index === 'number') { 30 | dataPoints = sliceDataPoints( 31 | state.hourly[index], 32 | state.dates[index], 33 | state.timezones[index], 34 | state.currently[index] 35 | ); 36 | } 37 | return { 38 | index, 39 | dataPoints, 40 | unitSystem: state.unitSystem 41 | }; 42 | } 43 | )(React.createClass({ 44 | onClosePress() { 45 | store.closeDetails(); 46 | }, 47 | render() { 48 | const { 49 | index, 50 | dataPoints, 51 | unitSystem 52 | } = this.props; 53 | if (index === null) { 54 | return null; 55 | } 56 | return view( 57 | { 58 | style: { 59 | position: 'absolute', 60 | left: 0, 61 | width, 62 | top: 0, 63 | height, 64 | zIndex: 100 65 | } 66 | }, 67 | background({index}), 68 | view( 69 | { 70 | // blurType: 'dark', 71 | style: { 72 | flex: 1 73 | } 74 | }, 75 | touchableOpacity( 76 | { 77 | style: { 78 | position: 'absolute', 79 | zIndex: 2, 80 | right: -2, 81 | top: statusBarHeight, 82 | width: 42, 83 | height: 42, 84 | justifyContent: 'center', 85 | alignItems: 'center' 86 | }, 87 | onPress: this.onClosePress 88 | }, 89 | closeIcon() 90 | ), 91 | view( 92 | { 93 | style: { 94 | flex: .35 95 | } 96 | }, 97 | view( 98 | { 99 | style: { 100 | position: 'absolute', 101 | bottom: -20, 102 | left: 0, 103 | right: 0, 104 | zIndex: 2 105 | } 106 | }, 107 | date({ 108 | index, 109 | opacity: 1 110 | }) 111 | ), 112 | locality({index}) 113 | ), 114 | view( 115 | { 116 | style: { 117 | flex: .65, 118 | transform: [ 119 | // {scale: 0.9}//, 120 | // {translateY: 20}, 121 | // {translateX: -20} 122 | ], 123 | justifyContent: 'flex-end' 124 | } 125 | }, 126 | view( 127 | { 128 | style: { 129 | height: 50 130 | // backgroundColor: 'blue' 131 | } 132 | }, 133 | detailsCloudCoverage({dataPoints}) 134 | ), 135 | view( 136 | { 137 | style: { 138 | height: 75//, 139 | // backgroundColor: 'green' 140 | } 141 | }, 142 | detailsPrecip({dataPoints}) 143 | ), 144 | view( 145 | { 146 | style: { 147 | height: 60//, 148 | // backgroundColor: 'orange' 149 | } 150 | }, 151 | detailsWind({dataPoints, unitSystem}) 152 | ), 153 | detailsTemperature({dataPoints}), 154 | hourScale({hours: [2, 6, 10, 14, 18, 22]}) 155 | ) 156 | ) 157 | ); 158 | } 159 | })); 160 | -------------------------------------------------------------------------------- /components/detailsCloudCoverage.js: -------------------------------------------------------------------------------- 1 | /** 2 | * svg group with wind by hour 3 | * @module detailsCloudCoverage 4 | */ 5 | 6 | 'use strict'; 7 | 8 | const React = require('react'); 9 | const {width} = require('../lib/getDimensions')(); 10 | const height = 50; 11 | const svg = React.createFactory(require('react-native-svg').Svg); 12 | const Defs = React.createFactory(require('react-native-svg').Defs); 13 | const Stop = React.createFactory(require('react-native-svg').Stop); 14 | const LinearGradient = React.createFactory( 15 | require('react-native-svg').LinearGradient 16 | ); 17 | const {scaleLinear, scaleThreshold} = require('d3-scale'); 18 | const {area, curveMonotoneX} = require('d3-shape'); 19 | const path = React.createFactory(require('react-native-svg').Path); 20 | const setStateAnimated = require('../lib/setStateAnimated'); 21 | const view = React.createFactory(require('react-native').View); 22 | const text = React.createFactory(require('./text')); 23 | const labelTypes = [ 24 | 'clear', 'partly-cloudy', 'cloudy' 25 | ]; 26 | const labelTypeLables = { 27 | 'clear': 'Clear', 28 | 'partly-cloudy': 'Partly Cloudy', 29 | 'cloudy': 'Cloudy' 30 | }; 31 | const cloudCoverScale = scaleThreshold() 32 | .domain([.2, 1]) 33 | .range(labelTypes); 34 | const store = require('../reducers/main'); 35 | 36 | const labelWidth = 100; 37 | 38 | /** 39 | * @param {Object[]} points 40 | * @return {Object} point 41 | */ 42 | function getLabelPoint(points) { 43 | const slicePadding = Math.floor(xScale.invert(labelWidth / 2)); 44 | const slicedPoints = points.slice( 45 | slicePadding, 46 | points.length - 1 - slicePadding 47 | ); 48 | let currentGroup = [points[0]]; 49 | let groups = [currentGroup]; 50 | slicedPoints.slice(1).forEach((point) => { 51 | if (point.cloudCover === currentGroup[0].cloudCover) { 52 | currentGroup.push(point); 53 | } else { 54 | currentGroup = [point]; 55 | groups.push(currentGroup); 56 | } 57 | }); 58 | let biggestGroup = groups[0]; 59 | groups.forEach((group) => { 60 | if (group.length > biggestGroup.length) { 61 | biggestGroup = group; 62 | } 63 | }); 64 | let biggestGroupMiddleIndex = Math.floor(biggestGroup.length / 2); 65 | return biggestGroup[biggestGroupMiddleIndex]; 66 | } 67 | 68 | /** 69 | * @param {Object} props 70 | * @return {Object} state 71 | */ 72 | function getSateFromProps(props) { 73 | let { 74 | dataPoints 75 | } = props; 76 | if (dataPoints.length === 24) { 77 | let points = dataPoints.map(({cloudCover}, hour) => { 78 | return { 79 | cloudCover: Math.round((cloudCover || 0) * 4) / 4, 80 | labelType: cloudCoverScale(cloudCover), 81 | hour 82 | } 83 | }); 84 | return { 85 | labelPoint: getLabelPoint(points), 86 | points, 87 | valid: true 88 | }; 89 | } 90 | return { 91 | valid: false 92 | }; 93 | } 94 | 95 | const xScale = scaleLinear() 96 | .domain([0, 23]) 97 | .range([0, width]); 98 | 99 | const yScale = scaleLinear() 100 | .domain([0, 1]) 101 | .range([height, 0]); 102 | 103 | const areaFn = area() 104 | .y0(p => yScale(p.cloudCover)) 105 | .y1(height) 106 | .x((p) => xScale(p.hour)) 107 | .curve(curveMonotoneX); 108 | 109 | module.exports = React.createClass({ 110 | getInitialState: function () { 111 | return Object.assign( 112 | { 113 | points: null, 114 | valid: false, 115 | animationProgress: 1 116 | }, 117 | getSateFromProps(this.props) 118 | ); 119 | }, 120 | componentWillMount: function () { 121 | this.setStateAnimated = setStateAnimated(500, () => { 122 | store.configureAnimation(); 123 | }); 124 | }, 125 | componentWillReceiveProps: function (props) { 126 | const newState = getSateFromProps(props); 127 | if (!newState.valid) { 128 | this.setState(newState); 129 | return; 130 | } 131 | this.setState({animationProgress: 0}); 132 | setTimeout(() => { 133 | newState.animationProgress = 1; 134 | this.setStateAnimated(newState); 135 | }) 136 | }, 137 | render: function () { 138 | const {points, labelPoint, animationProgress, valid} = this.state; 139 | if (!points) { 140 | return null; 141 | } 142 | const d = areaFn(points); 143 | return view( 144 | { 145 | width, 146 | height 147 | }, 148 | (animationProgress === 1 && valid) ? text( 149 | { 150 | style: { 151 | position: 'absolute', 152 | top: 20, 153 | left: xScale(labelPoint.hour) - labelWidth / 2, 154 | width: labelWidth, 155 | fontSize: 12, 156 | textAlign: 'center' 157 | } 158 | }, 159 | labelTypeLables[labelPoint.labelType] 160 | ) : null, 161 | svg( 162 | { 163 | width, 164 | height 165 | }, 166 | Defs(null, 167 | LinearGradient( 168 | { 169 | id: 'grad', 170 | x1: 0, 171 | y1: 0, 172 | x2: 0, 173 | y2: height 174 | }, 175 | Stop({ 176 | offset: String(0), 177 | stopColor: 'rgba(255, 255, 255)', 178 | stopOpacity: .1 179 | }), 180 | Stop({ 181 | offset: String(1), 182 | stopColor: 'rgba(255, 255, 255)', 183 | stopOpacity: 0 184 | }) 185 | ) 186 | ), 187 | path({ 188 | d, 189 | strokeWidth: 0, 190 | fill: 'url(#grad)' 191 | }) 192 | ) 193 | ); 194 | } 195 | }); 196 | -------------------------------------------------------------------------------- /components/detailsPrecip.js: -------------------------------------------------------------------------------- 1 | /** 2 | * svg group with wind by hour 3 | * @module detailsPrecip 4 | */ 5 | 6 | 'use strict'; 7 | 8 | const React = require('react'); 9 | const {width} = require('../lib/getDimensions')(); 10 | const height = 50; 11 | const svg = React.createFactory(require('react-native-svg').Svg); 12 | const circle = React.createFactory(require('react-native-svg').Circle); 13 | const svgText = React.createFactory(require('react-native-svg').Text); 14 | const {scaleLinear, scaleQuantize} = require('d3-scale'); 15 | const view = React.createFactory(require('react-native').View); 16 | const text = React.createFactory(require('./text')); 17 | 18 | const labelWidth = 100; 19 | const xScale = scaleLinear() 20 | .domain([0, 23]) 21 | .range([6, width - 6]); 22 | const fontWeightScale = scaleQuantize().domain([2.5, 7.6]).range(['200', '400', '600']); 23 | const radiusScale = scaleQuantize().domain([2.5, 7.6]).range([2, 3, 4]); 24 | const levelScale = scaleQuantize().domain([0, 1]).range([1, 2, 3]); 25 | 26 | function getMaxProbabilityPoint(points) { 27 | const slicePadding = Math.floor(xScale.invert(labelWidth / 2)); 28 | const slicedPoints = points.slice( 29 | slicePadding, 30 | points.length - 1 - slicePadding 31 | ); 32 | let maxProbabilityPoint = slicedPoints[0]; 33 | for (let i = 1; i < slicedPoints.length; i++) { 34 | if (maxProbabilityPoint.probability < slicedPoints[i].probability) { 35 | maxProbabilityPoint = slicedPoints[i]; 36 | } 37 | } 38 | return maxProbabilityPoint; 39 | } 40 | 41 | /** 42 | * @param {Object} props 43 | * @return {Object} state 44 | */ 45 | function getSateFromProps(props) { 46 | let { 47 | dataPoints 48 | } = props; 49 | if (dataPoints.length === 24) { 50 | let points = dataPoints.map((dp, key) => { 51 | let rain = (( 52 | dp.icon.match('rain') || 53 | dp.icon.match('sleet') 54 | ) && dp.precipIntensity) ? 1 : 0; 55 | let snow = dp.icon.match('snow') ? 1 : 0; 56 | return { 57 | key, 58 | rain, 59 | snow, 60 | intensity: dp.precipIntensity || 0, 61 | probability: dp.precipProbability || 0 62 | } 63 | }); 64 | return { 65 | maxProbabilityPoint: getMaxProbabilityPoint(points), 66 | points 67 | }; 68 | } 69 | return { 70 | points: null 71 | }; 72 | } 73 | 74 | module.exports = React.createClass({ 75 | getInitialState: function () { 76 | return Object.assign( 77 | {points: null}, 78 | getSateFromProps(this.props) 79 | ); 80 | }, 81 | componentWillReceiveProps: function (props) { 82 | this.setState(getSateFromProps(props)); 83 | }, 84 | render: function () { 85 | const {points, maxProbabilityPoint} = this.state; 86 | if (!points) { 87 | return null; 88 | } 89 | return view( 90 | {}, 91 | ( 92 | levelScale(maxProbabilityPoint.probability) > 0 && 93 | (maxProbabilityPoint.rain || maxProbabilityPoint.snow) 94 | ) ? text( 95 | { 96 | style: { 97 | position: 'relative', 98 | left: xScale(maxProbabilityPoint.key) - labelWidth / 2, 99 | width: labelWidth, 100 | fontSize: 12, 101 | textAlign: 'center', 102 | paddingBottom: 5 103 | } 104 | }, 105 | `${ 106 | maxProbabilityPoint.snow ? 107 | 'Snow' : 108 | 'Rain' 109 | } ${Math.round(maxProbabilityPoint.probability * 100)}%` 110 | ) : null, 111 | svg( 112 | { 113 | key: 'detailsPrecip', 114 | width, 115 | height 116 | }, 117 | points.map(p => { 118 | let x = xScale(p.key); 119 | let y0 = 6; 120 | let symbols = []; 121 | const step = width / 23; 122 | if (p.intensity) { 123 | for ( 124 | let i = 0; 125 | i < levelScale(p.probability); 126 | i++ 127 | ) { 128 | let y = y0 + i * step; 129 | if (p.rain) { 130 | symbols.push(circle({ 131 | key: p.key + i, 132 | r: radiusScale(p.intensity), 133 | cx: x, 134 | cy: y, 135 | fill: 'white' 136 | })); 137 | } else if (p.snow) { 138 | symbols.push(svgText( 139 | { 140 | key: p.key + i, 141 | x: x, 142 | y: y - 11.75,//adjust to center 143 | fontSize: 28, 144 | fontWeight: fontWeightScale(p.intensity), 145 | fill: 'white', 146 | textAnchor: 'middle' 147 | }, 148 | '*' 149 | )); 150 | } 151 | } 152 | } 153 | return symbols; 154 | }) 155 | ) 156 | ); 157 | } 158 | }); 159 | -------------------------------------------------------------------------------- /components/detailsTemperature.js: -------------------------------------------------------------------------------- 1 | /** 2 | * temperature curve 3 | * @module detailsTemperature 4 | */ 5 | 6 | 'use strict'; 7 | 8 | const React = require('react'); 9 | const {width} = require('../lib/getDimensions')(); 10 | const dTHeight = 130; 11 | const connect = require('react-redux').connect; 12 | const svg = React.createFactory(require('react-native-svg').Svg); 13 | const {line, curveMonotoneX, curveBasis, area} = require('d3-shape'); 14 | const formatTemperature = require('../lib/format-temperature'); 15 | const path = React.createFactory(require('react-native-svg').Path); 16 | const g = React.createFactory(require('react-native-svg').G); 17 | const svgText = React.createFactory(require('react-native-svg').Text); 18 | const circle = React.createFactory(require('react-native-svg').Circle); 19 | const {scaleLinear} = require('d3-scale'); 20 | const view = React.createFactory(require('react-native').View); 21 | const setStateAnimated = require('../lib/setStateAnimated'); 22 | const store = require('../reducers/main'); 23 | 24 | /** 25 | * @param {number[]} tempAr 26 | * @return {{localMax: Number, localMin: Number}} 27 | */ 28 | function getLocalMinMax(tempAr) { 29 | let localMax = Math.max(...tempAr); 30 | let localMin = Math.min(...tempAr); 31 | return {localMax, localMin}; 32 | } 33 | 34 | /** 35 | * @param {Object[]} temperaturePointAr 36 | * @returns {Object[]} temperaturePointAr 37 | */ 38 | function addSpecialPoints(temperaturePointAr) { 39 | let {localMin, localMax} = getLocalMinMax( 40 | temperaturePointAr.map(p => p.t) 41 | ); 42 | temperaturePointAr.forEach(p => { 43 | if (p.t === localMax) { 44 | p.localMax = true; 45 | localMax = null; 46 | } 47 | if (p.t === localMin) { 48 | p.localMin = true; 49 | localMin = null; 50 | } 51 | }) 52 | return temperaturePointAr; 53 | } 54 | 55 | /** 56 | * @param {object[]} dataPoints 57 | * @return {object[]} with only needed values 58 | */ 59 | function getPoints(dataPoints) { 60 | return dataPoints.map((dp, hour) => { 61 | return { 62 | t: dp.temperature, 63 | at: dp.apparentTemperature, 64 | hour, 65 | time: dp.time, 66 | current: dp.current 67 | } 68 | }) 69 | } 70 | 71 | let xScale = scaleLinear() 72 | .domain([0, 23]) 73 | .range([0, width]); 74 | 75 | let topPadding = 45; 76 | let bottomPadding = 22; 77 | let yScale = scaleLinear() 78 | .range([dTHeight - bottomPadding, topPadding]); 79 | 80 | let tAreaFn = area() 81 | .y0(p => yScale(p.t)) 82 | .y1(() => dTHeight) 83 | .x(p => xScale(p.hour)) 84 | .curve(curveMonotoneX); 85 | let atAreaFn = area() 86 | .y0(p => yScale(p.at)) 87 | .y1(() => dTHeight) 88 | .x(p => xScale(p.hour)) 89 | .curve(curveMonotoneX); 90 | 91 | 92 | /** 93 | * @param {Object} props 94 | * @return {Object} state 95 | */ 96 | function getSateFromProps(props) { 97 | let { 98 | dataPoints 99 | } = props; 100 | if (dataPoints.length === 24) { 101 | let points = addSpecialPoints( 102 | getPoints(dataPoints) 103 | ); 104 | points.forEach((p) => { 105 | p.at = Math.round(p.at); 106 | p.r = Math.round(p.t); 107 | }); 108 | let tPoints = [].concat( 109 | points.map((p) => p.t), 110 | points.map((p) => p.at) 111 | ) 112 | return { 113 | minTemperture: Math.min(...tPoints), 114 | maxTemperture: Math.max(...tPoints), 115 | points, 116 | valid: true 117 | }; 118 | } else { 119 | return { 120 | valid: false 121 | }; 122 | } 123 | } 124 | 125 | const labelWidth = 40; 126 | 127 | /** 128 | * @param {object[]} labeledPoints 129 | * @param {function} xScale d3-scale 130 | * @return {number[]} position of labels 131 | */ 132 | function getLabelPoints(labeledPoints, xScale) { 133 | const labelPositions = []; 134 | labeledPoints.forEach((p, key) => { 135 | let min = (key + 1) * labelWidth; 136 | if (labelPositions.length) { 137 | min = labelPositions[key - 1] +labelWidth; 138 | } 139 | const max = width - (labeledPoints.length - key) * labelWidth; 140 | labelPositions.push(Math.min(max, Math.max(min, xScale(p.hour)))); 141 | }); 142 | return labelPositions; 143 | } 144 | 145 | let labelsAnchorLineFn = line() 146 | .x(p => p[0]) 147 | .y(p => p[1]) 148 | .curve(curveBasis); 149 | 150 | /** 151 | * @param {object} p 152 | * @return {string} 153 | */ 154 | function getLabelPrefix(p) { 155 | if (p.current) { 156 | return 'Current'; 157 | } 158 | if (p.localMin) { 159 | return 'Min'; 160 | } 161 | if (p.localMax) { 162 | return 'Max'; 163 | } 164 | } 165 | 166 | module.exports = connect( 167 | state => { 168 | return { 169 | temperatureFormat: state.temperatureFormat 170 | // useApparentTemperature: state.useApparentTemperature 171 | } 172 | } 173 | )(React.createClass({ 174 | getInitialState: function () { 175 | return Object.assign( 176 | { 177 | points: null, 178 | minTemperture: null, 179 | maxTemperture: null, 180 | valid: false, 181 | animationProgress: 1 182 | }, 183 | getSateFromProps(this.props) 184 | ); 185 | }, 186 | componentWillMount: function () { 187 | this.setStateAnimated = setStateAnimated(500, () => { 188 | store.configureAnimation(); 189 | }); 190 | }, 191 | componentWillReceiveProps: function (props) { 192 | const newState = getSateFromProps(props); 193 | if (!newState.valid) { 194 | this.setState(newState); 195 | return; 196 | } 197 | this.setState({animationProgress: 0}); 198 | setTimeout(() => { 199 | newState.animationProgress = 1; 200 | this.setStateAnimated(newState); 201 | }) 202 | }, 203 | render: function () { 204 | const { 205 | minTemperture, 206 | maxTemperture, 207 | points, 208 | animationProgress, 209 | valid 210 | } = this.state; 211 | const {temperatureFormat/*, useApparentTemperature*/} = this.props; 212 | if (!points) { 213 | return null; 214 | } 215 | yScale.domain([minTemperture, maxTemperture]); 216 | let tArea = tAreaFn(points); 217 | let atArea = atAreaFn(points); 218 | let labeledPoints = points.filter((p) => { 219 | return p.current || p.localMin || p.localMax; 220 | }); 221 | let labelPoints = getLabelPoints(labeledPoints, xScale); 222 | return view( 223 | { 224 | style: { 225 | width, 226 | height: dTHeight 227 | } 228 | }, 229 | svg( 230 | { 231 | width, 232 | height: dTHeight, 233 | style: {position: 'absolute', top: 0} 234 | }, 235 | path({ 236 | d: tArea, 237 | strokeWidth: 0, 238 | fill: 'rgba(255, 255, 255, .3)' 239 | }), 240 | path({ 241 | d: atArea, 242 | strokeWidth: 0, 243 | fill: 'rgba(255, 255, 255, .1)' 244 | }) 245 | ), 246 | (animationProgress === 1 && valid) ? svg( 247 | { 248 | width, 249 | height: dTHeight, 250 | style: {position: 'absolute', top: 0} 251 | }, 252 | labeledPoints.map((p, key) => { 253 | let elems = [] 254 | let anchorP = [xScale(p.hour), yScale(Math.round(p.t))]; 255 | let labelP = [labelPoints[key], 32]; 256 | let anchorLine = labelsAnchorLineFn( 257 | [ 258 | anchorP, 259 | [anchorP[0], anchorP[1] - 10], 260 | [labelP[0], labelP[1] + 10], 261 | labelP 262 | ] 263 | ); 264 | elems.push(path({ 265 | d: anchorLine, 266 | stroke: 'white', 267 | strokeWidth: 1, 268 | fill: 'transparent', 269 | opacity: 0.3 270 | })); 271 | elems.push(circle({ 272 | r: 3, 273 | cx: anchorP[0], 274 | cy: anchorP[1], 275 | fill: 'white', 276 | strokeWidth: p.current ? 6 : 0, 277 | stroke: 'rgba(255, 255, 255, .2)' 278 | })) 279 | elems.push(svgText( 280 | { 281 | x: labelP[0], 282 | y: 0, 283 | fontSize: 12, 284 | fill: 'white', 285 | textAnchor: 'middle' 286 | }, 287 | `${getLabelPrefix(p)}` 288 | )); 289 | elems.push(svgText( 290 | { 291 | x: labelP[0] - (p.t ? 5 : 0), 292 | y: 14, 293 | fontSize: 12, 294 | fill: 'white', 295 | textAnchor: 'middle' 296 | }, 297 | `${formatTemperature( 298 | p.t, temperatureFormat 299 | )}` 300 | )); 301 | return g({key}, ...elems); 302 | }) 303 | ) : null 304 | ) 305 | } 306 | })); 307 | -------------------------------------------------------------------------------- /components/detailsWind.js: -------------------------------------------------------------------------------- 1 | /** 2 | * svg group with wind by hour 3 | * @module detailsWind 4 | */ 5 | 6 | 'use strict'; 7 | 8 | const React = require('react'); 9 | const {width} = require('../lib/getDimensions')(); 10 | const dWheight = 32; 11 | const path = React.createFactory(require('react-native-svg').Path); 12 | const svg = React.createFactory(require('react-native-svg').Svg); 13 | const view = React.createFactory(require('react-native').View); 14 | const text = React.createFactory(require('./text')); 15 | const {scaleLinear, scaleThreshold} = require('d3-scale'); 16 | const d3Path = require('d3-path').path; 17 | 18 | //scales 19 | const beaufort = require('../lib/beaufort'); 20 | const beaufortLabel = scaleThreshold() 21 | .domain([ 2, 3, 4, 5, 6, 7, 8]) 22 | .range(['Calm', 'Light wind', 'Wind', 'Wind', 'Wind', 'Strong wind', 'Strong wind', 'Storm']); 23 | const y0 = dWheight / 2; 24 | const l = 15; 25 | const xScale = scaleLinear() 26 | .domain([0, 23]) 27 | .range([l / 1.5, width - l / 1.5]); 28 | const strokeWidthScale = scaleThreshold() 29 | .domain([ 2, 3, 4, 5, 6, 7, 8]) 30 | .range([ 0, .5, 1, 1.5, 2, 2.5, 3, 4]); 31 | 32 | const labelWidth = 130; 33 | 34 | /** 35 | * @param {Object[]} points 36 | * @return {Object} 37 | */ 38 | function getExtremPoint(points) { 39 | const slicePadding = Math.floor(xScale.invert(labelWidth / 2)); 40 | const slicedPoints = points.slice( 41 | slicePadding, 42 | points.length - 1 - slicePadding 43 | ); 44 | let extremPoint = slicedPoints[0]; 45 | for (let i = 1; i < slicedPoints.length; i++) { 46 | if (extremPoint.windSpeed < slicedPoints[i].windSpeed) { 47 | extremPoint = slicedPoints[i]; 48 | } 49 | } 50 | return extremPoint; 51 | } 52 | 53 | /** 54 | * @param {Object} props 55 | * @return {Object} state 56 | */ 57 | function getSateFromProps(props) { 58 | let { 59 | dataPoints 60 | } = props; 61 | if (dataPoints.length === 24) { 62 | let points = dataPoints.map((dp, hour) => { 63 | let b = beaufort(dp.windSpeed); 64 | let strokeWidth = strokeWidthScale(b); 65 | return { 66 | hour, 67 | windSpeed: dp.windSpeed, 68 | b, 69 | strokeWidth 70 | } 71 | }); 72 | return { 73 | extremPoint: getExtremPoint(points), 74 | points 75 | }; 76 | } 77 | return { 78 | points: null 79 | }; 80 | } 81 | 82 | /** 83 | * @param {number} speed m/s 84 | * @param {string} unitSystem us or metric 85 | * @return {string} 86 | */ 87 | function formatWindSpeed(speed, unitSystem) { 88 | switch (unitSystem) { 89 | case 'us': 90 | return Math.round(speed * 3.6 / 1.60934) + '\u202Fmph' 91 | default: 92 | return Math.round(speed) + '\u202Fm/s' 93 | } 94 | } 95 | 96 | module.exports = React.createClass({ 97 | getInitialState: function () { 98 | return Object.assign( 99 | {points: null}, 100 | getSateFromProps(this.props) 101 | ); 102 | }, 103 | componentWillReceiveProps: function (props) { 104 | this.setState(getSateFromProps(props)); 105 | }, 106 | render: function () { 107 | const {points, extremPoint} = this.state; 108 | const {unitSystem} = this.props; 109 | if (!points) { 110 | return null; 111 | } 112 | return view( 113 | {}, 114 | extremPoint.strokeWidth > 0 ? view( 115 | {}, 116 | text( 117 | { 118 | style: { 119 | position: 'relative', 120 | left: xScale(extremPoint.hour) - labelWidth / 2, 121 | width: labelWidth, 122 | fontSize: 12, 123 | textAlign: 'center' 124 | } 125 | }, 126 | `${ 127 | beaufortLabel(extremPoint.b) 128 | } ${ 129 | formatWindSpeed(extremPoint.windSpeed, unitSystem) 130 | // Math.round(extremPoint.windSpeed) 131 | }` 132 | ) 133 | ) : null, 134 | svg( 135 | { 136 | key: 'detailsPrecip', 137 | width, 138 | height: dWheight 139 | }, 140 | points.map(({hour, strokeWidth}) => { 141 | let x = xScale(hour); 142 | let key = hour; 143 | let curve = d3Path(); 144 | curve.moveTo(x, y0 - l / 2); 145 | curve.lineTo(x + l / 2, y0); 146 | curve.lineTo(x, y0 + l / 2); 147 | let d = curve.toString(); 148 | return path({ 149 | key, 150 | stroke: 'white', 151 | strokeWidth, 152 | fill: 'transparent', 153 | d 154 | }); 155 | }) 156 | ) 157 | ); 158 | } 159 | }); 160 | -------------------------------------------------------------------------------- /components/dropIcon.js: -------------------------------------------------------------------------------- 1 | /** 2 | * delete svg icon 3 | * @module components/dropIcon 4 | */ 5 | 6 | const React = require('react'); 7 | const svg = React.createFactory(require('react-native-svg').Svg); 8 | const path = React.createFactory(require('react-native-svg').Path); 9 | const circle = React.createFactory(require('react-native-svg').Circle); 10 | 11 | module.exports = React.createClass({ 12 | render: function () { 13 | return svg( 14 | { 15 | width: 32, 16 | height: 32 17 | }, 18 | circle( 19 | { 20 | cx: 16, 21 | cy: 16, 22 | r: 10.5, 23 | fill: 'red' 24 | } 25 | ), 26 | path( 27 | { 28 | d: 'M10,16.5 L21,16.5', 29 | stroke: 'white', 30 | strokeWidth: 1 31 | } 32 | ) 33 | ); 34 | } 35 | }); 36 | -------------------------------------------------------------------------------- /components/hourScale.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @module components/hourScale 3 | */ 4 | 5 | 'use strict'; 6 | 7 | const React = require('react'); 8 | const view = React.createFactory(require('react-native').View); 9 | const text = React.createFactory(require('./text')); 10 | const connect = require('react-redux').connect; 11 | module.exports = connect((state) => { 12 | const {is12h} = state; 13 | return { 14 | is12h 15 | }; 16 | })(React.createClass({ 17 | render() { 18 | const {hours, is12h} = this.props; 19 | if (is12h === null) { 20 | return null; 21 | } 22 | return view( 23 | { 24 | style: { 25 | position: 'absolute', 26 | bottom: 5, 27 | left: 0, 28 | right: 0, 29 | height: 15, 30 | flexDirection: 'row' 31 | } 32 | }, 33 | hours.map((h, key) => { 34 | let sufix = ''; 35 | let prefix = String(h); 36 | if (is12h) { 37 | if (h <= 12) { 38 | sufix = 'am'; 39 | } else { 40 | sufix = 'pm'; 41 | } 42 | prefix = String(h % 12); 43 | } else { 44 | if ( 45 | key === 0 || 46 | key === hours.length - 1 47 | ) { 48 | sufix = 'h'; 49 | } 50 | } 51 | return text( 52 | { 53 | key, 54 | style: { 55 | flex: 1, 56 | textAlign: 'center', 57 | height: 15, 58 | lineHeight: 15, 59 | fontSize: 10 60 | } 61 | }, 62 | prefix + sufix 63 | ); 64 | }) 65 | ) 66 | } 67 | })); 68 | -------------------------------------------------------------------------------- /components/inAppStore.js: -------------------------------------------------------------------------------- 1 | /** 2 | * store with in app purchases 3 | * @module components/inAppStore 4 | */ 5 | 6 | const React = require('react'); 7 | // const closeIcon = React.createFactory(require('./closeIcon')); 8 | const text = React.createFactory(require('./text')); 9 | const modal = React.createFactory(require('react-native').Modal); 10 | const view = React.createFactory(require('react-native').View); 11 | const activityIndicator = React.createFactory(require('react-native').ActivityIndicator); 12 | const {width, height/*, statusBarHeight*/} = require('../lib/getDimensions')(); 13 | const touchable = React.createFactory( 14 | require('react-native').TouchableWithoutFeedback 15 | ); 16 | const touchableOpacity = React.createFactory( 17 | require('react-native').TouchableOpacity 18 | ); 19 | const connect = require('react-redux').connect; 20 | const store = require('../reducers/main'); 21 | 22 | module.exports = connect( 23 | function mapStateToProps({ 24 | inAppStore, 25 | forecastApiLimit, 26 | forecastApiRequests, 27 | products 28 | }) { 29 | return { 30 | inAppStore, 31 | requestLeft: forecastApiLimit - forecastApiRequests, 32 | products 33 | }; 34 | } 35 | )(React.createClass({ 36 | render() { 37 | const { 38 | inAppStore, 39 | requestLeft, 40 | products 41 | } = this.props; 42 | if (!inAppStore) { 43 | return null; 44 | } 45 | const shop = inAppStore === 'shop' || null; 46 | const leave = inAppStore === 'leave' || null; 47 | return modal( 48 | { 49 | transparent: true, 50 | supportedOrientations: ['portrait'], 51 | animationType: 'fade' 52 | }, 53 | view( 54 | { 55 | style: { 56 | flex: 1, 57 | justifyContent: 'center', 58 | padding: 20 59 | } 60 | }, 61 | touchable( 62 | { 63 | onPress: () => { 64 | store.hideStore(); 65 | } 66 | }, 67 | view( 68 | { 69 | style: { 70 | backgroundColor: 'rgba(0, 0, 0, 0.5)', 71 | position: 'absolute', 72 | top: 0, 73 | left: 0, 74 | width, 75 | height 76 | } 77 | } 78 | ) 79 | ), 80 | view( 81 | { 82 | style: { 83 | borderRadius: 10, 84 | backgroundColor: 'white', 85 | padding: 20, 86 | overflow: 'hidden', 87 | alignItems: 'center' 88 | } 89 | }, 90 | view( 91 | { 92 | alignItems: 'center' 93 | }, 94 | text( 95 | { 96 | style: { 97 | fontWeight: '200', 98 | color: 'black', 99 | fontSize: 40 100 | } 101 | }, 102 | requestLeft 103 | ), 104 | text( 105 | { 106 | style: { 107 | color: 'black', 108 | fontWeight: '200', 109 | fontSize: 20 110 | } 111 | }, 112 | 'weather requests left' 113 | ), 114 | text( 115 | { 116 | style: { 117 | marginTop: 10, 118 | textAlign: 'center', 119 | fontWeight: '200', 120 | color: 'black' 121 | } 122 | }, 123 | shop ? `To show you weather Zowni needs to pay for access to weather data from Dark Sky` : 124 | leave ? `Thank you for purchase! Enjoy the app!` : null 125 | ) 126 | ), 127 | shop && view( 128 | { 129 | style: { 130 | height: 100, 131 | justifyContent: 'center' 132 | } 133 | }, 134 | products && products.length ? products.map((p, key) => { 135 | return view( 136 | { 137 | key, 138 | style: { 139 | flexDirection: 'row', 140 | alignItems: 'center', 141 | paddingLeft: 40, 142 | paddingRight: 40 143 | } 144 | }, 145 | text( 146 | { 147 | style: { 148 | color: 'black', 149 | width: 140 150 | } 151 | }, 152 | `Get ${p.title}` 153 | ), 154 | touchableOpacity( 155 | { 156 | onPress: () => { 157 | store.purchaseProduct( 158 | p.identifier 159 | ) 160 | } 161 | }, 162 | text( 163 | { 164 | style: { 165 | width: 60, 166 | lineHeight: 25, 167 | borderRadius: 5, 168 | marginLeft: 20, 169 | textAlign: 'center', 170 | color: 'rgb(17, 107, 255)', 171 | borderWidth: 1, 172 | borderColor: 'rgb(17, 107, 255)' 173 | } 174 | }, 175 | `${p.price}${p.currencySymbol}` 176 | ) 177 | ) 178 | ) 179 | }) : activityIndicator() 180 | ), 181 | touchableOpacity( 182 | { 183 | onPress: () => { 184 | store.hideStore(); 185 | } 186 | }, 187 | text( 188 | { 189 | style: { 190 | marginTop: leave ? 20 : 0, 191 | color: 'rgb(17, 107, 255)', 192 | fontSize: 18 193 | } 194 | }, 195 | shop ? 'Later' : 'Continue' 196 | ) 197 | ) 198 | ) 199 | ) 200 | ) 201 | } 202 | })); 203 | -------------------------------------------------------------------------------- /components/legend.js: -------------------------------------------------------------------------------- 1 | /** 2 | * temperature legend 3 | * @module components/legend 4 | */ 5 | 6 | const React = require('react'); 7 | const view = React.createFactory(require('react-native').View); 8 | const text = React.createFactory(require('./text')); 9 | const connect = require('react-redux').connect; 10 | const temperatureColor = require('../lib/temperature-color'); 11 | const formatTemperature = require('../lib/format-temperature'); 12 | 13 | /** 14 | * returns temperature domain with pretty numbers 15 | * @param {string} temperatureFormat 16 | * @return {number[]} 17 | */ 18 | function getDomain(temperatureFormat) { 19 | const domain = temperatureColor.domain().slice(1, -1); //drop first and last 20 | if (temperatureFormat === 'F') { 21 | return domain.map(tC => { 22 | let tF = tC * 1.8 + 32; 23 | return Math.round(tF / 5) * 5; 24 | }); 25 | } else { 26 | return domain; 27 | } 28 | } 29 | 30 | /** 31 | * @param {number} t temperature C or F 32 | * @param {string} temperatureFormat 33 | * @return {string} color 34 | */ 35 | function getColor(t, temperatureFormat) { 36 | if (temperatureFormat === 'F') { 37 | return temperatureColor((t - 32) / 1.8); 38 | } else { 39 | return temperatureColor(t); 40 | } 41 | } 42 | 43 | module.exports = connect( 44 | function mapStateToProps(state) { 45 | return { 46 | temperatureFormat: state.temperatureFormat 47 | } 48 | } 49 | )(React.createClass({ 50 | render: function () { 51 | const {temperatureFormat} = this.props; 52 | const domain = getDomain(temperatureFormat); 53 | return view( 54 | { 55 | flex: 1, 56 | height: 28 57 | }, 58 | view( 59 | { 60 | style: { 61 | position: 'absolute', 62 | top: 0, 63 | left: 0, 64 | right: 0, 65 | flexDirection: 'row', 66 | height: 28 67 | } 68 | }, 69 | domain.map((t) => { 70 | return view( 71 | { 72 | key: t, 73 | style: { 74 | flex: 1, 75 | height: 28, 76 | backgroundColor: getColor(t, temperatureFormat) 77 | } 78 | } 79 | ); 80 | }) 81 | ), 82 | view( 83 | { 84 | style: { 85 | position: 'absolute', 86 | top: 0, 87 | left: 0, 88 | right: 0, 89 | flexDirection: 'row', 90 | height: 28 91 | } 92 | }, 93 | domain.map((t) => { 94 | return text( 95 | { 96 | key: t, 97 | style: { 98 | flex: 1, 99 | lineHeight: 28, 100 | fontSize: 12, 101 | textAlign: 'center' 102 | } 103 | }, 104 | formatTemperature(t, 'C') // becouse temp is already converted 105 | ); 106 | }) 107 | ) 108 | ); 109 | } 110 | })); 111 | -------------------------------------------------------------------------------- /components/locality.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @module components/locality 3 | */ 4 | 5 | const React = require('react'); 6 | const text = React.createFactory(require('./text')); 7 | const connect = require('react-redux').connect; 8 | const getFeatureLabel = require('../lib/getFeatureLabel'); 9 | const view = React.createFactory( 10 | require('react-native').View 11 | ); 12 | 13 | const scaleLinear = require('d3-scale').scaleLinear; 14 | 15 | const get2ColumnsSize = scaleLinear().range( 16 | [32, 32, 16, 16] 17 | ).domain( 18 | [0, 9, 18, Infinity] 19 | ); 20 | 21 | const get1ColumnsSize = scaleLinear().range( 22 | [40, 40, 24, 24] 23 | ).domain( 24 | [0, 15, 30, Infinity] 25 | ); 26 | 27 | /** 28 | * @param {object} feature 29 | * @return {string} 30 | */ 31 | function getLabel(feature) { 32 | return getFeatureLabel(feature).split(',')[0]; 33 | } 34 | 35 | module.exports = connect( 36 | function mapStateToProps(state) { 37 | return { 38 | selectedLoacalities: state.selectedLoacalities 39 | } 40 | } 41 | )(React.createClass({ 42 | render: function () { 43 | const {selectedLoacalities, index} = this.props; 44 | if (!selectedLoacalities.length) { 45 | return null; 46 | } 47 | let label1; 48 | let label2; 49 | if (typeof index === 'number') { 50 | label1 = getLabel( 51 | selectedLoacalities[index] || selectedLoacalities[0] 52 | ); 53 | } else { 54 | label1 = getLabel(selectedLoacalities[0]); 55 | label2 = getLabel(selectedLoacalities[1]); 56 | } 57 | const maxLength = Math.max(...label1.split(' ').concat( 58 | label2 ? label2.split(' ') : [] 59 | ).map(l => l.length)); 60 | return view( 61 | { 62 | style: { 63 | flex: 1, 64 | justifyContent: 'center' 65 | } 66 | }, 67 | label2 ? 68 | view( 69 | { 70 | style: { 71 | flexDirection: 'row', 72 | alignItems: 'center' 73 | } 74 | }, 75 | view( 76 | { 77 | style: { 78 | flex: 1 79 | } 80 | }, 81 | text( 82 | { 83 | style: { 84 | textAlign: 'center', 85 | fontSize: get2ColumnsSize(maxLength), 86 | fontWeight: '200' 87 | }, 88 | ellipsizeMode: 'middle', 89 | numberOfLines: 2 90 | }, 91 | label1 92 | ) 93 | ), 94 | view( 95 | { 96 | style: { 97 | flex: 1 98 | } 99 | }, 100 | text( 101 | { 102 | style: { 103 | textAlign: 'center', 104 | fontSize: get2ColumnsSize(maxLength), 105 | fontWeight: '200' 106 | }, 107 | ellipsizeMode: 'middle', 108 | numberOfLines: 2 109 | }, 110 | label2 111 | ) 112 | ) 113 | ) : 114 | view( 115 | {}, 116 | text( 117 | { 118 | style: { 119 | textAlign: 'center', 120 | fontSize: get1ColumnsSize(maxLength), 121 | fontWeight: '200', 122 | paddingLeft: 15, 123 | paddingRight: 15 124 | }, 125 | ellipsizeMode: 'middle', 126 | numberOfLines: 2 127 | }, 128 | label1 129 | ) 130 | ) 131 | ); 132 | } 133 | })); 134 | -------------------------------------------------------------------------------- /components/locationIcon.js: -------------------------------------------------------------------------------- 1 | /** 2 | * location icon 3 | * @module location icon 4 | */ 5 | 6 | 'use strict'; 7 | 8 | let React = require('react'); 9 | let svg = React.createFactory(require('react-native-svg').Svg); 10 | let path = React.createFactory(require('react-native-svg').Path); 11 | 12 | module.exports = React.createClass({ 13 | render() { 14 | return svg( 15 | { 16 | width: 22, 17 | height: 22, 18 | viewBox: '0 0 32 32' 19 | }, 20 | path({ 21 | fill: 'white', 22 | d: 'M16.53 8.327c-2.077 0-3.77 1.656-3.77 3.69 0 2.036 1.693 3.692 3.77 3.692 2.08 0 3.77-1.657 3.77-3.692s-1.69-3.69-3.77-3.69zm0 6.328c-1.484 0-2.692-1.183-2.692-2.637s1.208-2.636 2.693-2.636 2.693 1.182 2.693 2.636c0 1.454-1.208 2.637-2.692 2.637z' 23 | }), 24 | path({ 25 | fill: 'white', 26 | d: 'M23.737 4.962C21.787 3.052 19.193 2 16.435 2s-5.35 1.052-7.3 2.962c-3.61 3.533-4.06 10.182-.973 14.21l8.273 11.7 8.26-11.683c3.1-4.046 2.65-10.695-.958-14.228zm.086 13.61L16.435 29.02l-7.4-10.464c-2.798-3.655-2.397-9.66.86-12.85 1.748-1.71 4.07-2.65 6.54-2.65 2.47 0 4.793.94 6.54 2.65 3.258 3.19 3.66 9.195.848 12.867z' 27 | }) 28 | ); 29 | } 30 | }); 31 | -------------------------------------------------------------------------------- /components/menuIcon.js: -------------------------------------------------------------------------------- 1 | /** 2 | * menu icon 3 | * @module menu icon 4 | */ 5 | 6 | 'use strict'; 7 | 8 | let React = require('react'); 9 | let svg = React.createFactory(require('react-native-svg').Svg); 10 | let path = React.createFactory(require('react-native-svg').Path); 11 | let g = React.createFactory(require('react-native-svg').G); 12 | 13 | module.exports = React.createClass({ 14 | render() { 15 | let y = 1; 16 | let d = 4; 17 | return svg( 18 | { 19 | width: 22, 20 | height: 22 21 | }, 22 | g( 23 | { 24 | stroke: 'white', 25 | strokeWidth: 1 26 | }, 27 | path({ 28 | y: (y+=d), 29 | d: 'M0,0 L22,0' 30 | }), 31 | path({ 32 | y: (y+=d), 33 | d: 'M0,0 L22,0' 34 | }), 35 | path({ 36 | y: (y+=d), 37 | d: 'M0,0 L22,0' 38 | }), 39 | path({ 40 | y: (y+=d), 41 | d: 'M0,0 L22,0' 42 | }) 43 | ) 44 | ); 45 | } 46 | }); 47 | -------------------------------------------------------------------------------- /components/next.js: -------------------------------------------------------------------------------- 1 | /** 2 | * next/prev icon 3 | * @module next 4 | */ 5 | 6 | 'use strict'; 7 | 8 | let React = require('react'); 9 | let {Dimensions} = require('react-native'); 10 | let view = React.createFactory(require('react-native').View); 11 | let text = React.createFactory(require('./text')); 12 | let svg = React.createFactory(require('react-native-svg').Svg); 13 | let path = React.createFactory(require('react-native-svg').Path); 14 | let g = React.createFactory(require('react-native-svg').G); 15 | let {scaleLinear} = require('d3-scale'); 16 | 17 | const size = 32; 18 | 19 | let getOpacity = scaleLinear() 20 | .domain([0, size, 50, Infinity]) 21 | .range([0, 0.5, 1, 1]); 22 | module.exports = React.createClass({ 23 | render: function () { 24 | let {width, height} = Dimensions.get('window'); 25 | const {scroll, index} = this.props; 26 | let scrollAbs = Math.abs(scroll); 27 | let scale = 1; 28 | let left = index ? width : -scrollAbs; 29 | return view( 30 | { 31 | style: { 32 | position: 'absolute', 33 | opacity: getOpacity(scrollAbs), 34 | left, 35 | top: height * 0.5 - 16, 36 | width: scrollAbs, 37 | alignItems: 'center' 38 | } 39 | }, 40 | view( 41 | { 42 | style: { 43 | height: size, 44 | width: size, 45 | transform: [ 46 | { 47 | 'scale': scale 48 | } 49 | ] 50 | } 51 | }, 52 | text({ 53 | style: { 54 | position: 'absolute', 55 | fontSize: 9, 56 | width: size, 57 | textAlign: 'center', 58 | left: index ? -1 : 2, 59 | lineHeight: size 60 | } 61 | }, '24h'), 62 | svg( 63 | { 64 | width: size, 65 | height: size 66 | }, 67 | g( 68 | { 69 | fill: 'white', 70 | strokeWidth: 0 71 | }, 72 | index ? 73 | path({d: 'M15 29C7.832 29 2 23.168 2 16S7.832 3 15 3s13 5.832 13 13c0 .41-.332.743-.743.743-.41 0-.744-.332-.744-.743 0-6.35-5.165-11.515-11.514-11.515C8.65 4.485 3.483 9.65 3.483 16c0 6.35 5.165 11.514 11.515 11.514 4.06 0 7.866-2.177 9.93-5.682.208-.354.663-.47 1.016-.262.354.207.47.663.264 1.017C23.882 26.542 19.586 29 15 29z'}) : 74 | path({d: 'M17 29c7.168 0 13-5.832 13-13S24.168 3 17 3 4 8.832 4 16c0 .41.332.743.743.743.41 0 .744-.332.744-.743C5.487 9.65 10.652 4.485 17 4.485c6.35 0 11.516 5.166 11.516 11.515 0 6.35-5.165 11.514-11.515 11.514-4.06 0-7.866-2.177-9.93-5.682-.208-.354-.663-.47-1.016-.262-.354.207-.47.663-.264 1.017C8.118 26.542 12.414 29 17 29z'}), 75 | index ? 76 | path({d: 'M27.31 17c-.127 0-.255-.035-.37-.107l-4.58-2.846c-.348-.216-.464-.688-.258-1.054.205-.365.653-.487 1-.27l3.997 2.483 2.57-3.877c.232-.35.687-.434 1.017-.192.33.243.412.722.182 1.07l-2.96 4.462c-.143.214-.37.33-.6.33z'}) : 77 | path({d: 'M4.69 17c.127 0 .255-.035.37-.107l4.58-2.846c.348-.216.464-.688.258-1.054-.205-.365-.653-.487-1-.27L4.9 15.206 2.33 11.33c-.232-.35-.687-.434-1.017-.192-.33.243-.412.722-.182 1.07l2.96 4.462c.143.214.37.33.6.33z'}) 78 | ) 79 | ) 80 | ) 81 | ); 82 | } 83 | }); 84 | -------------------------------------------------------------------------------- /components/nextIcon.js: -------------------------------------------------------------------------------- 1 | /** 2 | * next icon for menus 3 | * @module next icon 4 | */ 5 | 6 | 'use strict'; 7 | 8 | let React = require('react'); 9 | let svg = React.createFactory(require('react-native-svg').Svg); 10 | let path = React.createFactory(require('react-native-svg').Path); 11 | 12 | module.exports = React.createClass({ 13 | render() { 14 | return svg( 15 | { 16 | width: 32, 17 | height: 32, 18 | style: { 19 | transform: [{scale: 0.5}] 20 | } 21 | }, 22 | path({ 23 | fill: 'white', 24 | viewBox: '0 0 32 32', 25 | d: 'M25.1 15.182l-1.484-.212L9.05 29.536l1.414 1.414 15.45-15.45-1.414-1.414-1.06 1.06h.706l-13.51-13.51v.708l.708-.707h-.707l14.217 14.217.353-.354-.132-.133.026-.185zm-.954.672l-.424.14 1.485.213.707-.707L10.99.577 9.578 1.99l14.216 14.217 1.06-1.06h-.707l.708.707v-.708L10.11 29.89h.708l-.707-.708v.707l14.036-14.036z' 26 | }) 27 | ); 28 | } 29 | }); 30 | -------------------------------------------------------------------------------- /components/options.js: -------------------------------------------------------------------------------- 1 | /** 2 | * city selector 3 | * @module components/options 4 | */ 5 | 6 | const React = require('react'); 7 | const text = React.createFactory(require('./text')); 8 | const nextIcon = React.createFactory(require('./nextIcon')); 9 | const view = React.createFactory(require('react-native').View); 10 | const segmentedControlIOS = React.createFactory( 11 | require('react-native').SegmentedControlIOS 12 | ); 13 | const {Linking, AlertIOS, Dimensions} = require('react-native'); 14 | const legend = React.createFactory(require('./legend')); 15 | const connect = require('react-redux').connect; 16 | const store = require('../reducers/main'); 17 | const touchableOpacity = React.createFactory( 18 | require('react-native').TouchableOpacity 19 | ); 20 | const scrollView = React.createFactory(require('react-native').ScrollView); 21 | const linearGradient = React.createFactory( 22 | require('react-native-svg').LinearGradient 23 | ); 24 | const svg = React.createFactory( 25 | require('react-native-svg').Svg 26 | ); 27 | const defs = React.createFactory(require('react-native-svg').Defs); 28 | const stop = React.createFactory(require('react-native-svg').Stop); 29 | const rect = React.createFactory(require('react-native-svg').Rect); 30 | const {ShareDialog, MessageDialog, AppEventsLogger} = require('react-native-fbsdk'); 31 | const linkObj = { 32 | contentType: 'link', 33 | contentUrl: 'http://zowni.com' 34 | }; 35 | const reportError = require('../lib/reportError'); 36 | 37 | module.exports = connect( 38 | function mapStateToProps(state) { 39 | return { 40 | temperatureFormat: state.temperatureFormat, 41 | forecastApiRequests: state.forecastApiRequests, 42 | forecastApiLimit: state.forecastApiLimit 43 | } 44 | } 45 | )(React.createClass({ 46 | shareInMessanger: function () { 47 | Promise.all([ 48 | MessageDialog.canShow(linkObj), 49 | ShareDialog.canShow(linkObj) 50 | ]).then( 51 | ([messanger, facebook]) => { 52 | if (messanger) { 53 | return MessageDialog.show(linkObj); 54 | } else if (facebook) { 55 | return ShareDialog.show(linkObj); 56 | } else { 57 | AlertIOS.alert( 58 | 'Oh..', 59 | 'Looks like Facebook does not work for you.' 60 | ); 61 | } 62 | } 63 | ).then(result => { 64 | if ( 65 | result && 66 | !result.isCancelled 67 | ) { 68 | AlertIOS.alert('Thank you!'); 69 | AppEventsLogger.logEvent('fbsharesuccessfull'); 70 | } else { 71 | AlertIOS.alert('Thank you', 'for trying'); 72 | AppEventsLogger.logEvent('fbsharetry'); 73 | } 74 | }).catch(err => { 75 | reportError(err); 76 | }); 77 | }, 78 | render: function () { 79 | const { 80 | temperatureFormat, 81 | // onCitySelectPress, 82 | forecastApiLimit, 83 | forecastApiRequests 84 | } = this.props; 85 | const {width} = Dimensions.get('window'); 86 | const rowStyle = { 87 | padding: 10, 88 | paddingTop: 5, 89 | paddingBottom: 5, 90 | paddingLeft: 0, 91 | marginLeft: 20, 92 | flexDirection: 'row', 93 | alignItems: 'center', 94 | borderBottomWidth: 1, 95 | borderBottomColor: 'rgba(255, 255, 255, 0.1)', 96 | height: 50 97 | }; 98 | return view( 99 | { 100 | style: { 101 | height: 230 102 | } 103 | }, 104 | view( 105 | { 106 | style: { 107 | flexDirection: 'row', 108 | padding: 10, 109 | paddingBottom: 0 110 | } 111 | }, 112 | legend() 113 | ), 114 | svg( 115 | { 116 | width, 117 | height: 20, 118 | style: { 119 | position: 'absolute', 120 | top: 38, 121 | left: 0, 122 | right: 0, 123 | height: 20, 124 | zIndex: 2 125 | } 126 | }, 127 | defs(null, 128 | linearGradient( 129 | { 130 | id: 'grad', 131 | x1: 0, 132 | y1: 0, 133 | x2: 0, 134 | y2: 20 135 | }, 136 | stop({ 137 | offset: String(1), 138 | stopColor: 'black', 139 | stopOpacity: 0 140 | }), 141 | stop({ 142 | offset: String(0), 143 | stopColor: 'black', 144 | stopOpacity: 1 145 | }) 146 | ) 147 | ), 148 | rect( 149 | {x: 0, y: 0, width, height: 20, fill: 'url(#grad)'} 150 | ) 151 | ), 152 | svg( 153 | { 154 | width, 155 | height: 20, 156 | style: { 157 | position: 'absolute', 158 | bottom: 0, 159 | left: 0, 160 | right: 0, 161 | height: 20, 162 | zIndex: 2 163 | } 164 | }, 165 | defs(null, 166 | linearGradient( 167 | { 168 | id: 'grad', 169 | x1: 0, 170 | y1: 0, 171 | x2: 0, 172 | y2: 20 173 | }, 174 | stop({ 175 | offset: String(0), 176 | stopColor: 'black', 177 | stopOpacity: 0 178 | }), 179 | stop({ 180 | offset: String(1), 181 | stopColor: 'black', 182 | stopOpacity: 1 183 | }) 184 | ) 185 | ), 186 | rect( 187 | {x: 0, y: 0, width, height: 20, fill: 'url(#grad)'} 188 | ) 189 | ), 190 | scrollView( 191 | { 192 | style: { 193 | height: 192 194 | }, 195 | contentContainerStyle: { 196 | paddingTop: 10, 197 | paddingBottom: 10 198 | } 199 | }, 200 | view( 201 | { 202 | style: [rowStyle] 203 | }, 204 | view( 205 | {style: {flex: 1}}, 206 | text({}, 'Temperature unit') 207 | ), 208 | segmentedControlIOS({ 209 | style: {width: 75}, 210 | tintColor: '#3599dd', 211 | onChange: () => store.toggleTemperatureFormat(), 212 | selectedIndex: temperatureFormat === 'C' ? 0 : 1, 213 | values: ['C', 'F'] 214 | }) 215 | ), 216 | view( 217 | { 218 | style: [rowStyle] 219 | }, 220 | touchableOpacity( 221 | { 222 | style: { 223 | flexDirection: 'row', 224 | alignItems: 'center', 225 | flex: 1 226 | }, 227 | onPress: () => { 228 | store.showStore(); 229 | } 230 | }, 231 | view( 232 | { 233 | style: {flex: 1} 234 | }, 235 | text({}, 'Powered by Dark\u00A0Sky') 236 | ), 237 | view( 238 | { 239 | style: {marginRight: 0} 240 | }, 241 | text( 242 | { 243 | style: {color: 'grey'} 244 | }, 245 | `${ 246 | forecastApiLimit - forecastApiRequests 247 | } requests left` 248 | ) 249 | ), 250 | view( 251 | { 252 | style: { 253 | width: 75, 254 | height: 48, 255 | alignItems: 'flex-end', 256 | justifyContent: 'center' 257 | } 258 | }, 259 | text( 260 | { 261 | style: { 262 | color: '#3599dd', 263 | width: 75, 264 | textAlign: 'right' 265 | } 266 | }, 267 | 'Buy more' 268 | ) 269 | ) 270 | ) 271 | ), 272 | view( 273 | { 274 | style: [rowStyle] 275 | }, 276 | touchableOpacity( 277 | { 278 | style: { 279 | flexDirection: 'row', 280 | alignItems: 'center', 281 | flex: 1 282 | }, 283 | onPress: this.shareInMessanger 284 | }, 285 | view( 286 | { 287 | style: {flex: 1} 288 | }, 289 | text( 290 | {style: {color: '#3599dd'}}, 291 | 'Invite friends to test Zowni' 292 | ) 293 | ), 294 | nextIcon() 295 | ) 296 | ), 297 | view( 298 | { 299 | style: [rowStyle] 300 | }, 301 | touchableOpacity( 302 | { 303 | style: { 304 | flexDirection: 'row', 305 | alignItems: 'center', 306 | flex: 1 307 | }, 308 | onPress () { 309 | Linking.canOpenURL( 310 | 'mailto:bilonenko.v@gmail.com' 311 | ).then(supported => { 312 | if (supported) { 313 | Linking.openURL('mailto:bilonenko.v@gmail.com'); 314 | } else { 315 | AlertIOS.alert( 316 | 'Please, contact me directly to ' + 317 | 'bilonenko.v@gmail.com' 318 | ); 319 | } 320 | }) 321 | } 322 | }, 323 | view( 324 | { 325 | style: {flex: 1} 326 | }, 327 | text({}, 'Report problem') 328 | ), 329 | nextIcon() 330 | ) 331 | ) 332 | ) 333 | ); 334 | } 335 | })); 336 | -------------------------------------------------------------------------------- /components/overcast.js: -------------------------------------------------------------------------------- 1 | /** 2 | * svg group with wind by hour 3 | * @module windDetailed 4 | */ 5 | 6 | 'use strict'; 7 | 8 | const React = require('react'); 9 | const {width} = require('../lib/getDimensions')(); 10 | const connect = require('react-redux').connect; 11 | // let line = React.createFactory(require('react-native-svg').Line); 12 | // let path = React.createFactory(require('react-native-svg').Path); 13 | const rect = React.createFactory(require('react-native-svg').Rect); 14 | const sliceDataPoints = require('../lib/sliceDataPoints'); 15 | const g = React.createFactory(require('react-native-svg').G); 16 | const {scaleLinear/*, scaleThreshold*/} = require('d3-scale'); 17 | // const d3Path = require('d3-path').path; 18 | const svg = React.createFactory(require('react-native-svg').Svg); 19 | /** 20 | * @param {Object} props 21 | * @return {Object} state 22 | */ 23 | function getSateFromProps(props) { 24 | let { 25 | index, 26 | timezones, 27 | hourly, 28 | dates 29 | } = props; 30 | const dataPoints = sliceDataPoints( 31 | hourly[index], 32 | dates[index], 33 | timezones[index] 34 | ); 35 | if (dataPoints.length === 24) { 36 | let points = dataPoints.map(({cloudCover}, hour) => { 37 | return { 38 | cloudCover, 39 | hour 40 | } 41 | }); 42 | return {points}; 43 | } 44 | return { 45 | points: null 46 | }; 47 | } 48 | 49 | module.exports = connect( 50 | state => { 51 | return { 52 | hourly: state.hourly, 53 | timezones: state.timezones, 54 | dates: state.dates 55 | } 56 | } 57 | )(React.createClass({ 58 | getInitialState: function () { 59 | return getSateFromProps(this.props); 60 | }, 61 | componentWillReceiveProps: function (props) { 62 | this.setState(getSateFromProps(props)); 63 | }, 64 | render: function () { 65 | const {points} = this.state; 66 | const height = 20; 67 | if (!points) { 68 | return null; 69 | } 70 | const xScale = scaleLinear() 71 | .domain([0, 23]) 72 | .range([0, width / 2]); 73 | const dxScale = scaleLinear() 74 | .domain([0, 1]) 75 | .range([0, width / 2 / 24]); 76 | return svg( 77 | { 78 | width: width/2, 79 | height, 80 | style: { 81 | position: 'absolute' 82 | } 83 | }, 84 | g( 85 | { 86 | opacity: .3 87 | }, 88 | points.map(({cloudCover, hour}, key) => { 89 | let x = Math.floor(xScale(hour)); 90 | let dx = Math.ceil(dxScale(cloudCover)); 91 | return rect({ 92 | key, 93 | x, 94 | y: 0, 95 | width: dx, 96 | height, 97 | fill: 'white' 98 | }) 99 | }) 100 | ) 101 | ); 102 | } 103 | })); 104 | -------------------------------------------------------------------------------- /components/rain.js: -------------------------------------------------------------------------------- 1 | /** 2 | * rain icon 3 | * @module rain 4 | */ 5 | 6 | 'use strict'; 7 | 8 | let React = require('react'); 9 | let View = React.createFactory(require('react-native').View); 10 | let Svg = React.createFactory(require('react-native-svg').Svg); 11 | let Line = React.createFactory(require('react-native-svg').Line); 12 | let scale = require('d3-scale').scaleQuantize(); 13 | let getLines = scale.domain([0, 1]).range([2, 3, 4, 5, 6]); 14 | 15 | module.exports = React.createClass({ 16 | getInitialState: function () { 17 | return { 18 | lines: 0 19 | } 20 | }, 21 | componentWillMount: function () { 22 | const {dataPoints} = this.props; 23 | let rainPoints = dataPoints.filter((p) => { 24 | return ( 25 | p.icon.match('rain') || 26 | p.icon.match('sleet') 27 | ) && p.precipIntensity; 28 | }); 29 | if (rainPoints.length === 0) { 30 | return; 31 | } 32 | let maxIntencity = Math.max(...rainPoints.map(p => p.precipIntensity)); 33 | let lines = getLines(maxIntencity); 34 | this.setState({lines}); 35 | }, 36 | render: function () { 37 | let {lines} = this.state; 38 | if (lines < 2) { 39 | return null; 40 | } 41 | const step = 4; 42 | let linesAr = []; 43 | for (let i = 0; i < lines; i++) { 44 | linesAr.push(Line({ 45 | key: i, 46 | x1: (i + 1) * step + 1, 47 | y1: 0, 48 | x2: i * step + 1, 49 | y2: 16, 50 | stroke: 'white', 51 | strokeWidth: 1 52 | })); 53 | } 54 | return View( 55 | { 56 | style: { 57 | height: 20, 58 | alignItems: 'center' 59 | } 60 | }, 61 | Svg( 62 | { 63 | width: lines * step + 2, 64 | height: 16 65 | }, 66 | linesAr 67 | ) 68 | ); 69 | } 70 | }); 71 | -------------------------------------------------------------------------------- /components/reload.js: -------------------------------------------------------------------------------- 1 | /** 2 | * refresh control 3 | * @module next 4 | */ 5 | 6 | 'use strict'; 7 | 8 | let React = require('react'); 9 | const getDimensions = require('../lib/getDimensions'); 10 | let view = React.createFactory(require('react-native').View); 11 | let text = React.createFactory(require('./text')); 12 | let {scaleLinear} = require('d3-scale'); 13 | let getOpacity = scaleLinear() 14 | .domain([0, 20, 50, Infinity]) 15 | .range([0, 0.5, 1, 1]); 16 | module.exports = React.createClass({ 17 | render: function () { 18 | const {viewHeight, statusBarHeight} = getDimensions(); 19 | const {scroll} = this.props; 20 | const scrollAbs = Math.max(Math.abs(scroll) - statusBarHeight, 0); 21 | return view( 22 | { 23 | style: { 24 | position: 'absolute', 25 | opacity: getOpacity(scrollAbs), 26 | bottom: viewHeight, 27 | left: 0, 28 | right: 0, 29 | height: scrollAbs, 30 | justifyContent: 'center', 31 | alignItems: 'center' 32 | } 33 | }, 34 | text( 35 | { 36 | style: { 37 | } 38 | }, 39 | 'RELOAD' 40 | ) 41 | ); 42 | } 43 | }); 44 | -------------------------------------------------------------------------------- /components/selectIcon.js: -------------------------------------------------------------------------------- 1 | /** 2 | * delete svg icon 3 | * @module components/dropIcon 4 | */ 5 | 6 | const React = require('react'); 7 | const svg = React.createFactory(require('react-native-svg').Svg); 8 | const circle = React.createFactory(require('react-native-svg').Circle); 9 | // const g = React.createFactory(require('react-native-svg').G); 10 | // const path = React.createFactory(require('react-native-svg').Path); 11 | 12 | module.exports = React.createClass({ 13 | render: function () { 14 | const {selected} = this.props; 15 | return svg( 16 | { 17 | width: 32, 18 | height: 32 19 | }, 20 | // !selected && g( 21 | // {}, 22 | // path( 23 | // { 24 | // d: 'M16.5,10 L16.5,22', 25 | // stroke: '#4A90E2', 26 | // strokeWidth: 1 27 | // } 28 | // ), 29 | // path( 30 | // { 31 | // d: 'M10,16.5 L22,16.5', 32 | // stroke: '#4A90E2', 33 | // strokeWidth: 1 34 | // } 35 | // ) 36 | // ), 37 | selected && circle( 38 | { 39 | cx: 16, 40 | cy: 16, 41 | r: 6.5, 42 | strokeWidth: 0, 43 | fill: '#4A90E2' 44 | } 45 | ) 46 | ); 47 | } 48 | }); 49 | -------------------------------------------------------------------------------- /components/snow.js: -------------------------------------------------------------------------------- 1 | /** 2 | * snow icon 3 | * @module snow 4 | */ 5 | 6 | 'use strict'; 7 | 8 | let React = require('react'); 9 | let View = React.createFactory(require('react-native').View); 10 | let Svg = React.createFactory(require('react-native-svg').Svg); 11 | let Path = React.createFactory(require('react-native-svg').Path); 12 | let G = React.createFactory(require('react-native-svg').G); 13 | 14 | module.exports = React.createClass({ 15 | getInitialState: function () { 16 | return { 17 | snow: false 18 | } 19 | }, 20 | componentWillMount: function () { 21 | const {dataPoints} = this.props; 22 | let snowPoints = dataPoints.filter((p) => { 23 | return p.icon.match('snow'); 24 | }); 25 | if (snowPoints.length) { 26 | this.setState({snow: true}); 27 | } 28 | }, 29 | render: function () { 30 | let {snow} = this.state; 31 | if (!snow) { 32 | return null; 33 | } 34 | return View( 35 | { 36 | style: { 37 | // backgroundColor: 'gold', 38 | height: 32, 39 | alignItems: 'center' 40 | } 41 | }, 42 | Svg( 43 | { 44 | width: 32, 45 | height: 32 46 | }, 47 | G( 48 | { 49 | stroke: 'white', 50 | strokeWidth: 1 51 | }, 52 | Path({d: 'M16,4 L16,13'}), 53 | Path({d: 'M16,27 L16,18'}), 54 | G( 55 | { 56 | rotate: 240, 57 | originX: 16, 58 | originY: 16 59 | }, 60 | Path({d: 'M16,4 L16,13'}), 61 | Path({d: 'M16,27 L16,18'}) 62 | ), 63 | G( 64 | { 65 | rotate: 120, 66 | originX: 16, 67 | originY: 16 68 | }, 69 | Path({d: 'M16,4 L16,13'}), 70 | Path({d: 'M16,27 L16,18'}) 71 | ) 72 | ) 73 | 74 | ) 75 | ); 76 | } 77 | }); 78 | -------------------------------------------------------------------------------- /components/t-bar.js: -------------------------------------------------------------------------------- 1 | /** 2 | * temperature bar 3 | * @module t-bar 4 | */ 5 | 6 | 'use strict'; 7 | 8 | const React = require('react'); 9 | const View = React.createFactory(require('react-native').View); 10 | const text = React.createFactory(require('./text')); 11 | const rain = React.createFactory(require('./rain')); 12 | const wind = React.createFactory(require('./wind')); 13 | const snow = React.createFactory(require('./snow')); 14 | const connect = require('react-redux').connect; 15 | const getDataPoints = require('../lib/getDataPoints'); 16 | const moment = require('moment'); 17 | const formatTemperature = require('../lib/format-temperature'); 18 | 19 | /** 20 | * takes correspomding data points from data block 21 | * @param {ForecastDataBlock} hourly 22 | * @param {number} startHour 23 | * @param {Date} date 24 | * @param {string} timezone 25 | * @returns {ForecastDataPoints} 26 | */ 27 | function sliceDataPoints(hourly, startHour, date, timezone) { 28 | const points = getDataPoints(hourly); 29 | if (!points) { 30 | return null; 31 | } 32 | let startTS = moment.tz(date, timezone).startOf('day').add(startHour, 'hour').unix(); 33 | let endTS = startTS + 24 / 4 * 3600; 34 | return hourly.data.filter(function (conditions) { 35 | let time = conditions.time; 36 | return time >= startTS && time < endTS; 37 | }); 38 | } 39 | 40 | const bottomPadding = 0.2; 41 | 42 | /** 43 | * @param {number} temperature 44 | * @param {number} minTemperture 45 | * @param {number} maxTemperture 46 | * @return {number} flex value 47 | */ 48 | function getBarHeight( 49 | temperature, 50 | minTemperture, 51 | maxTemperture 52 | ) { 53 | let barHeight = (temperature - minTemperture) / 54 | (maxTemperture - minTemperture) * (1 - bottomPadding); 55 | 56 | barHeight += bottomPadding; 57 | if (barHeight > 1) { 58 | barHeight = 1; 59 | } 60 | return barHeight; 61 | } 62 | 63 | /** 64 | * @param {number} temperatureC 65 | * @param {string} temperatureFormat C or F 66 | * @return {number} temp in C rounded to C or to F 67 | */ 68 | function roundTemperature(temperatureC, temperatureFormat) { 69 | if (temperatureFormat === 'F') { 70 | return Math.round(temperatureC * 1.8) / 1.8; 71 | } else { 72 | return Math.round(temperatureC); 73 | } 74 | } 75 | 76 | /** 77 | * @param {Object} props 78 | * @return {Object} state 79 | */ 80 | function getSateFromProps(props) { 81 | const { 82 | index, 83 | minTemperture, 84 | maxTemperture, 85 | temperatureFormat, 86 | timezones 87 | } = props; 88 | const hourly = props.hourly[index]; 89 | const dataPoints = sliceDataPoints( 90 | hourly, 91 | props.startHour, 92 | props.dates[index], 93 | timezones[index] 94 | ); 95 | if (!dataPoints || dataPoints.length !== 6) { 96 | return { 97 | temperature: NaN, 98 | dataPoints: null 99 | }; 100 | } 101 | let {temperature, apparentTemperature} = dataPoints[3]; 102 | 103 | return { 104 | tBarHeight: getBarHeight( 105 | roundTemperature(temperature, temperatureFormat), 106 | minTemperture, 107 | maxTemperture 108 | ), 109 | atBarHeight: getBarHeight( 110 | roundTemperature(apparentTemperature, temperatureFormat), 111 | minTemperture, 112 | maxTemperture 113 | ), 114 | temperature, 115 | dataPoints 116 | }; 117 | } 118 | 119 | module.exports = connect( 120 | (state) => { 121 | return { 122 | hourly: state.hourly, 123 | timezones: state.timezones, 124 | dates: state.dates, 125 | minTemperture: state.minTemperture, 126 | maxTemperture: state.maxTemperture, 127 | selectedBars: state.selectedBars, 128 | temperatureFormat: state.temperatureFormat, 129 | citySelect: state.citySelect, 130 | details: state.details 131 | }; 132 | } 133 | )(React.createClass({ 134 | getInitialState () { 135 | return Object.assign({ 136 | barHeight: 0 137 | }, getSateFromProps(this.props)); 138 | }, 139 | componentWillReceiveProps: function (props) { 140 | this.setState(getSateFromProps(props)); 141 | }, 142 | render: function () { 143 | const { 144 | temperatureFormat, width 145 | } = this.props; 146 | const {temperature, tBarHeight, dataPoints, atBarHeight} = this.state; 147 | if (!tBarHeight) { 148 | return null; 149 | } 150 | const temperatureStr = formatTemperature( 151 | temperature, temperatureFormat 152 | ); 153 | let atBarFlex = Math.min(atBarHeight / tBarHeight, 1); 154 | return View( 155 | { 156 | style: { 157 | width 158 | } 159 | }, 160 | View( 161 | { 162 | style: { 163 | flex: 1 - tBarHeight, 164 | justifyContent: 'flex-end' 165 | } 166 | }, 167 | dataPoints ? View( 168 | { 169 | style: { 170 | position: 'absolute', 171 | bottom: 0, 172 | left: 0, 173 | right: 0, 174 | justifyContent: 'flex-end' 175 | } 176 | }, 177 | wind({dataPoints}), 178 | snow({dataPoints}), 179 | rain({dataPoints}), 180 | text( 181 | { 182 | style: { 183 | textAlign: 'center', 184 | height: 18 185 | } 186 | }, 187 | temperatureStr 188 | ) 189 | ) : null 190 | ), 191 | View( 192 | { 193 | style: { 194 | flex: tBarHeight, 195 | justifyContent: 'flex-end' 196 | } 197 | }, 198 | View( 199 | { 200 | style: { 201 | flex: 1 - atBarFlex, 202 | backgroundColor: `rgba(255, 255, 255, 0.3)` 203 | } 204 | } 205 | ), 206 | View( 207 | { 208 | style: { 209 | flex: atBarFlex, 210 | backgroundColor: `rgba(255, 255, 255, 0.4)` 211 | } 212 | } 213 | ) 214 | ) 215 | ); 216 | } 217 | })); 218 | -------------------------------------------------------------------------------- /components/tFormat.js: -------------------------------------------------------------------------------- 1 | /** 2 | * swithcher for Celsius / Farenheit 3 | * @module components/tFormat 4 | */ 5 | 6 | const React = require('react'); 7 | const text = React.createFactory(require('./text')); 8 | const connect = require('react-redux').connect; 9 | const legend = React.createFactory(require('./legend')); 10 | const touchableOpacity = React.createFactory( 11 | require('react-native').TouchableOpacity 12 | ); 13 | const store = require('../reducers/main'); 14 | 15 | const textStyle = { 16 | padding: 2, 17 | fontSize: 18, 18 | lineHeight: 28, 19 | color: '#999999', 20 | fontWeight: '400' 21 | }; 22 | 23 | const highlightStyle = { 24 | color: 'white' 25 | }; 26 | 27 | module.exports = connect( 28 | function mapStateToProps(state) { 29 | return { 30 | temperatureFormat: state.temperatureFormat 31 | } 32 | } 33 | )(React.createClass({ 34 | render: function () { 35 | const {temperatureFormat} = this.props; 36 | return touchableOpacity( 37 | { 38 | style: { 39 | flexDirection: 'row', 40 | paddingLeft: 10, 41 | paddingRight: 10 42 | }, 43 | onPress: () => store.toggleTemperatureFormat() 44 | }, 45 | legend(), 46 | text( 47 | { 48 | style: [ 49 | textStyle, (temperatureFormat === 'C') && highlightStyle 50 | ] 51 | }, 52 | '°C' 53 | ), 54 | text( 55 | { 56 | style: { 57 | fontSize: 24, 58 | lineHeight: 28, 59 | fontWeight: '100', 60 | color: '#999999' 61 | } 62 | }, 63 | '/' 64 | ), 65 | text( 66 | { 67 | style: [ 68 | textStyle, (temperatureFormat === 'F') && highlightStyle 69 | ] 70 | }, 71 | '°F' 72 | ) 73 | ); 74 | } 75 | })); 76 | -------------------------------------------------------------------------------- /components/text.js: -------------------------------------------------------------------------------- 1 | /** 2 | * provides default styling for text 3 | * @module components/text 4 | */ 5 | 6 | let React = require('react'); 7 | let Text = React.createFactory(require('react-native').Text); 8 | let StyleSheet = require('react-native').StyleSheet; 9 | const styles = StyleSheet.create({ 10 | normal: { 11 | color: 'white', 12 | backgroundColor: 'transparent' 13 | }, 14 | link: { 15 | color: 'rgb(17, 107, 255)' 16 | } 17 | }) 18 | 19 | module.exports = React.createClass({ 20 | render: function () { 21 | const {style, ellipsizeMode, numberOfLines} = this.props; 22 | return Text( 23 | { 24 | style: [ 25 | styles.normal, 26 | styles[this.props.class] 27 | ].concat(style), 28 | ellipsizeMode, 29 | numberOfLines 30 | }, 31 | this.props.children 32 | ); 33 | } 34 | }); 35 | -------------------------------------------------------------------------------- /components/wind.js: -------------------------------------------------------------------------------- 1 | /** 2 | * wind icon 3 | * @module wind 4 | */ 5 | 6 | 'use strict'; 7 | 8 | let React = require('react'); 9 | let View = React.createFactory(require('react-native').View); 10 | let Animated = require('react-native').Animated; 11 | let AnimatedView = React.createFactory(Animated.View); 12 | let Svg = React.createFactory(require('react-native-svg').Svg); 13 | let Circle = React.createFactory(require('react-native-svg').Circle); 14 | let Path = React.createFactory(require('react-native-svg').Path); 15 | let G = React.createFactory(require('react-native-svg').G); 16 | const beaufort = require('../lib/beaufort'); 17 | const {scaleThreshold} = require('d3-scale'); 18 | const durationScale = scaleThreshold() 19 | .domain([ 3, 4, 5, 6, 7 ]) 20 | .range([ 6000, 6000, 3000, 1500, 750, 400]); 21 | 22 | /** 23 | * @param {number} windSpeed m/s 24 | * @return {number} animation duration ms 25 | */ 26 | function getDuration(windSpeed) { 27 | const b = beaufort(windSpeed); 28 | return durationScale(b); 29 | } 30 | 31 | module.exports = React.createClass({ 32 | getInitialState: function () { 33 | return { 34 | angle: new Animated.Value(0), 35 | show: false, 36 | maxWindSpeed: 0 37 | } 38 | }, 39 | componentWillMount: function () { 40 | let {dataPoints} = this.props; 41 | let windyPoints = dataPoints.filter((p) => { 42 | return p.icon.match('wind') || 43 | beaufort(p.windSpeed) >= 4; 44 | }); 45 | if (windyPoints.length) { 46 | let maxWindSpeed = Math.max( 47 | ...dataPoints.map(p => p.windSpeed) 48 | .filter(Boolean) 49 | ); 50 | this.setState({maxWindSpeed, show: true}); 51 | } 52 | }, 53 | componentDidMount: function () { 54 | let {angle, maxWindSpeed} = this.state; 55 | function cycle() { 56 | angle.setValue(0); 57 | Animated.timing( 58 | angle, 59 | { 60 | duration: getDuration(maxWindSpeed), 61 | toValue: 360, 62 | easing: a => a //do not ease 63 | } 64 | ).start(cycle); 65 | } 66 | cycle(); 67 | }, 68 | render: function () { 69 | let {show} = this.state; 70 | if (!show) { 71 | return null; 72 | } 73 | let rotateValue = this.state.angle.interpolate({ 74 | inputRange: [0, 360], 75 | outputRange: ['0deg', '360deg'] 76 | }); 77 | return View( 78 | { 79 | style: { 80 | height: 32, 81 | alignItems: 'center' 82 | } 83 | }, 84 | AnimatedView( 85 | { 86 | style: { 87 | width: 32, 88 | height: 32, 89 | transform: [{'rotate': rotateValue}] 90 | } 91 | }, 92 | Svg( 93 | { 94 | width: 32, 95 | height: 32 96 | }, 97 | G( 98 | { 99 | stroke: 'white', 100 | strokeWidth: 1 101 | }, 102 | Path({d: 'M5.5,21.5 L13,17'}), 103 | Path({d: 'M26.5,21.5 L19,17'}), 104 | Path({d: 'M16,4 L16,13'}) 105 | ), 106 | Circle({cx: 16, cy: 16, r: 1, fill: 'white'}) 107 | ) 108 | ) 109 | ); 110 | } 111 | }); 112 | -------------------------------------------------------------------------------- /index.android.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Sample React Native App 3 | * https://github.com/facebook/react-native 4 | */ 5 | 6 | import React, { Component } from 'react'; 7 | import { 8 | AppRegistry, 9 | StyleSheet, 10 | Text, 11 | View 12 | } from 'react-native'; 13 | 14 | class zowninative extends Component { 15 | render() { 16 | return ( 17 | 18 | 19 | Welcome to React Native! 20 | 21 | 22 | To get started, edit index.android.js 23 | 24 | 25 | Shake or press menu button for dev menu 26 | 27 | 28 | ); 29 | } 30 | } 31 | 32 | const styles = StyleSheet.create({ 33 | container: { 34 | flex: 1, 35 | justifyContent: 'center', 36 | alignItems: 'center', 37 | backgroundColor: '#F5FCFF', 38 | }, 39 | welcome: { 40 | fontSize: 20, 41 | textAlign: 'center', 42 | margin: 10, 43 | }, 44 | instructions: { 45 | textAlign: 'center', 46 | color: '#333333', 47 | marginBottom: 5, 48 | }, 49 | }); 50 | 51 | AppRegistry.registerComponent('zowninative', () => zowninative); 52 | -------------------------------------------------------------------------------- /index.ios.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @file entry point for ios app 3 | */ 4 | 5 | const React = require('react'); 6 | const {AppRegistry, StyleSheet} = require('react-native'); 7 | const getDimensions = require('./lib/getDimensions'); 8 | const scrollView = React.createFactory(require('react-native').ScrollView); 9 | const View = React.createFactory(require('react-native').View); 10 | const touchableOpacity = React.createFactory( 11 | require('react-native').TouchableOpacity 12 | ); 13 | const Provider = React.createFactory(require('react-redux').Provider); 14 | const store = require('./reducers/main'); 15 | const locality = React.createFactory(require('./components/locality')); 16 | const column = React.createFactory(require('./components/column')); 17 | const dateSelect = React.createFactory(require('./components/dateSelect')); 18 | const background = React.createFactory(require('./components/background')); 19 | const next = React.createFactory(require('./components/next')); 20 | const menuIcon = React.createFactory(require('./components/menuIcon')); 21 | const locationIcon = React.createFactory(require('./components/locationIcon')); 22 | const reload = React.createFactory(require('./components/reload')); 23 | const citySelect = React.createFactory(require('./components/city-select')); 24 | const details = React.createFactory(require('./components/details')); 25 | const options = React.createFactory(require('./components/options')); 26 | const inAppStore = React.createFactory(require('./components/inAppStore')); 27 | const styles = StyleSheet.create({ 28 | header: { 29 | flex: 0.35 30 | }, 31 | body: { 32 | flex: 0.65, 33 | flexDirection: 'row', 34 | flexWrap: 'nowrap' 35 | } 36 | }); 37 | 38 | let {scaleLinear} = require('d3-scale'); 39 | function getScrollOpacity(height, verticalScroll) { 40 | let scrollOpacityScale = scaleLinear() 41 | .domain([-10000, 0, height, 10000]) 42 | .range([0, 0, 1, 1]); 43 | let d = height - verticalScroll; 44 | return scrollOpacityScale(d); 45 | } 46 | 47 | let zowninative = React.createClass({ 48 | getInitialState: function () { 49 | return { 50 | scrollEnabled: true, 51 | menuOpen: false, 52 | refreshing: false, 53 | scroll: 0, 54 | verticalScroll: 0 55 | }; 56 | }, 57 | 58 | componentWillMount: function () { 59 | store.loadState(); 60 | }, 61 | componentDidUpdate: function () { 62 | let {scrollEnabled} = this.state; 63 | if (!scrollEnabled) { 64 | setTimeout(() => { 65 | this.setState({scrollEnabled: true}) 66 | }, 500); 67 | } 68 | }, 69 | onCitySelectPress: function () { 70 | if (this.verticalScrollView) { 71 | setTimeout(() => { 72 | this.verticalScrollView.scrollTo({ 73 | x: 0, 74 | y: 0 75 | }); 76 | }, 500); 77 | } 78 | }, 79 | onMenuPress: function () { 80 | this.verticalScrollView.scrollTo({x: 0, y: 230}) 81 | }, 82 | onLocationPress: function () { 83 | store.toggleCitySelect(); 84 | }, 85 | onVerticalScroll: function ({nativeEvent}) { 86 | const {scrollEnabled} = this.state; 87 | const y = nativeEvent.contentOffset.y; 88 | this.setState({verticalScroll: y}); 89 | if ( 90 | y < -70 && 91 | this.verticalScrollView && 92 | scrollEnabled 93 | ) { 94 | this.setState({ 95 | scrollEnabled: false 96 | }); 97 | store.initDates(); 98 | this.verticalScrollView.scrollTo({x: 0, y: 0}); 99 | } 100 | }, 101 | onHorizontalScroll: function ({nativeEvent}) { 102 | const {scrollEnabled} = this.state; 103 | let x = Math.abs(nativeEvent.contentOffset.x); 104 | this.setState({scroll: nativeEvent.contentOffset.x}); 105 | if ( 106 | x > 50 && 107 | this.horizontalScrollView && 108 | scrollEnabled 109 | ) { 110 | store.slideDates(nativeEvent.contentOffset.x); 111 | this.setState({ 112 | scrollEnabled: false 113 | }) 114 | this.horizontalScrollView.scrollTo({x: 0, y: 0}); 115 | } 116 | }, 117 | render: function () { 118 | let {scrollEnabled, scroll, verticalScroll} = this.state; 119 | let {width, viewHeight, statusBarHeight} = getDimensions(); 120 | return Provider( 121 | {store}, 122 | View( 123 | { 124 | style: { 125 | flex: 1, 126 | backgroundColor: 'black' 127 | } 128 | }, 129 | scrollView( 130 | { 131 | style: { 132 | width, 133 | height: viewHeight//, 134 | // marginTop: statusBarHeight 135 | }, 136 | scrollEnabled, 137 | directionalLockEnabled: true, 138 | snapToInterval: 230, 139 | ref: ref => {this.verticalScrollView = ref}, 140 | showsVerticalScrollIndicator: false, 141 | onScroll: this.onVerticalScroll, 142 | scrollEventThrottle: scrollEnabled ? 16 : 50 143 | }, 144 | View( 145 | { 146 | style: { 147 | width, 148 | height: viewHeight 149 | } 150 | }, 151 | reload({scroll: verticalScroll}), 152 | scrollView( 153 | { 154 | scrollEnabled, 155 | horizontal: true, 156 | ref: ref => {this.horizontalScrollView = ref}, 157 | onScroll: this.onHorizontalScroll, 158 | directionalLockEnabled: true, 159 | scrollEventThrottle: scrollEnabled ? 16 : 50, 160 | style: { 161 | width, 162 | height: viewHeight 163 | } 164 | }, 165 | next({ 166 | index: 0, 167 | scroll 168 | }), 169 | next({ 170 | index: 1, 171 | scroll 172 | }), 173 | background(), 174 | View( 175 | { 176 | style: { 177 | position: 'absolute', 178 | width, 179 | height: viewHeight 180 | } 181 | }, 182 | View( 183 | { 184 | position: 'absolute', 185 | top: statusBarHeight, 186 | opacity: getScrollOpacity(20, verticalScroll), 187 | left: -2, 188 | right: -2, 189 | height: 42, 190 | zIndex: 2, 191 | flexDirection: 'row', 192 | justifyContent: 'space-between' 193 | }, 194 | touchableOpacity( 195 | { 196 | style: { 197 | width: 42, 198 | height: 42, 199 | justifyContent: 'center', 200 | alignItems: 'center' 201 | }, 202 | onPress: this.onMenuPress 203 | }, 204 | menuIcon() 205 | ), 206 | touchableOpacity( 207 | { 208 | style: { 209 | width: 42, 210 | height: 42, 211 | justifyContent: 'center', 212 | alignItems: 'center' 213 | }, 214 | onPress: this.onLocationPress 215 | }, 216 | locationIcon() 217 | ) 218 | ), 219 | View( 220 | { 221 | style: [ 222 | styles.header, 223 | {opacity: getScrollOpacity( 224 | 80, 225 | verticalScroll 226 | )} 227 | ] 228 | }, 229 | locality() 230 | ), 231 | View( 232 | { 233 | style: styles.body 234 | }, 235 | column({ 236 | index: 0, 237 | dateOpacity: getScrollOpacity( 238 | viewHeight * 0.3, 239 | verticalScroll 240 | ) 241 | }), 242 | column({ 243 | index: 1, 244 | dateOpacity: getScrollOpacity( 245 | viewHeight * 0.3, 246 | verticalScroll 247 | ) 248 | }) 249 | ) 250 | ), 251 | details(), 252 | inAppStore() 253 | ) 254 | ), 255 | options({ 256 | onCitySelectPress: this.onCitySelectPress 257 | }) 258 | ), 259 | dateSelect(), 260 | citySelect() 261 | ) 262 | ); 263 | } 264 | }); 265 | 266 | AppRegistry.registerComponent('zowninative', () => zowninative); 267 | -------------------------------------------------------------------------------- /ios/zowninative.xcodeproj/xcshareddata/xcschemes/zowninative.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 29 | 35 | 36 | 37 | 38 | 39 | 44 | 45 | 47 | 53 | 54 | 55 | 56 | 57 | 63 | 64 | 65 | 66 | 67 | 68 | 78 | 80 | 86 | 87 | 88 | 89 | 90 | 91 | 97 | 99 | 105 | 106 | 107 | 108 | 110 | 111 | 114 | 115 | 116 | -------------------------------------------------------------------------------- /ios/zowninative/AppDelegate.h: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2015-present, Facebook, Inc. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the BSD-style license found in the 6 | * LICENSE file in the root directory of this source tree. An additional grant 7 | * of patent rights can be found in the PATENTS file in the same directory. 8 | */ 9 | 10 | #import 11 | 12 | @interface AppDelegate : UIResponder 13 | 14 | @property (nonatomic, strong) UIWindow *window; 15 | 16 | @end 17 | -------------------------------------------------------------------------------- /ios/zowninative/AppDelegate.m: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2015-present, Facebook, Inc. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the BSD-style license found in the 6 | * LICENSE file in the root directory of this source tree. An additional grant 7 | * of patent rights can be found in the PATENTS file in the same directory. 8 | */ 9 | 10 | #import "AppDelegate.h" 11 | 12 | #import "RCTBundleURLProvider.h" 13 | #import "RCTRootView.h" 14 | #import 15 | 16 | @implementation AppDelegate 17 | 18 | 19 | - (BOOL)application:(UIApplication *)application 20 | openURL:(NSURL *)url 21 | options:(NSDictionary *)options { 22 | 23 | BOOL handled = [[FBSDKApplicationDelegate sharedInstance] application:application 24 | openURL:url 25 | sourceApplication:options[UIApplicationOpenURLOptionsSourceApplicationKey] 26 | annotation:options[UIApplicationOpenURLOptionsAnnotationKey] 27 | ]; 28 | // Add any custom logic here. 29 | return handled; 30 | } 31 | 32 | - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions 33 | { 34 | [[FBSDKApplicationDelegate sharedInstance] application:application 35 | didFinishLaunchingWithOptions:launchOptions]; 36 | 37 | NSURL *jsCodeLocation; 38 | 39 | jsCodeLocation = [[RCTBundleURLProvider sharedSettings] jsBundleURLForBundleRoot:@"index.ios" fallbackResource:nil]; 40 | // jsCodeLocation = [NSURL URLWithString:@"http://192.168.188.22:8081/index.ios.bundle?platform=ios&dev=true"]; //uncomment this line 41 | 42 | RCTRootView *rootView = [[RCTRootView alloc] initWithBundleURL:jsCodeLocation 43 | moduleName:@"zowninative" 44 | initialProperties:nil 45 | launchOptions:launchOptions]; 46 | rootView.backgroundColor = [[UIColor alloc] initWithRed:1.0f green:1.0f blue:1.0f alpha:1]; 47 | 48 | self.window = [[UIWindow alloc] initWithFrame:[UIScreen mainScreen].bounds]; 49 | UIViewController *rootViewController = [UIViewController new]; 50 | rootViewController.view = rootView; 51 | self.window.rootViewController = rootViewController; 52 | [self.window makeKeyAndVisible]; 53 | return YES; 54 | } 55 | 56 | - (void)applicationDidBecomeActive:(UIApplication *)application { 57 | [FBSDKAppEvents activateApp]; 58 | } 59 | 60 | @end 61 | -------------------------------------------------------------------------------- /ios/zowninative/Base.lproj/LaunchScreen.xib: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /ios/zowninative/Images-2.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "iphone", 5 | "size" : "29x29", 6 | "scale" : "2x" 7 | }, 8 | { 9 | "idiom" : "iphone", 10 | "size" : "29x29", 11 | "scale" : "3x" 12 | }, 13 | { 14 | "idiom" : "iphone", 15 | "size" : "40x40", 16 | "scale" : "2x" 17 | }, 18 | { 19 | "idiom" : "iphone", 20 | "size" : "40x40", 21 | "scale" : "3x" 22 | }, 23 | { 24 | "size" : "60x60", 25 | "idiom" : "iphone", 26 | "filename" : "icon@2x.png", 27 | "scale" : "2x" 28 | }, 29 | { 30 | "size" : "60x60", 31 | "idiom" : "iphone", 32 | "filename" : "icon@3x.png", 33 | "scale" : "3x" 34 | } 35 | ], 36 | "info" : { 37 | "version" : 1, 38 | "author" : "xcode" 39 | } 40 | } -------------------------------------------------------------------------------- /ios/zowninative/Images-2.xcassets/AppIcon.appiconset/icon@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/delfrrr/weather-app-react-native/b0837e82ad96f76a9a52da306e4a6f9f21f32825/ios/zowninative/Images-2.xcassets/AppIcon.appiconset/icon@2x.png -------------------------------------------------------------------------------- /ios/zowninative/Images-2.xcassets/AppIcon.appiconset/icon@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/delfrrr/weather-app-react-native/b0837e82ad96f76a9a52da306e4a6f9f21f32825/ios/zowninative/Images-2.xcassets/AppIcon.appiconset/icon@3x.png -------------------------------------------------------------------------------- /ios/zowninative/Images-2.xcassets/Brand Assets.launchimage/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "orientation" : "portrait", 5 | "idiom" : "iphone", 6 | "filename" : "iPhone SE Lounch Screen@2x.png", 7 | "extent" : "full-screen", 8 | "minimum-system-version" : "7.0", 9 | "scale" : "2x" 10 | }, 11 | { 12 | "extent" : "full-screen", 13 | "idiom" : "iphone", 14 | "subtype" : "retina4", 15 | "filename" : "iPhone SE Lounch Screen@2x-1.png", 16 | "minimum-system-version" : "7.0", 17 | "orientation" : "portrait", 18 | "scale" : "2x" 19 | } 20 | ], 21 | "info" : { 22 | "version" : 1, 23 | "author" : "xcode" 24 | } 25 | } -------------------------------------------------------------------------------- /ios/zowninative/Images-2.xcassets/Brand Assets.launchimage/iPhone SE Lounch Screen@2x-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/delfrrr/weather-app-react-native/b0837e82ad96f76a9a52da306e4a6f9f21f32825/ios/zowninative/Images-2.xcassets/Brand Assets.launchimage/iPhone SE Lounch Screen@2x-1.png -------------------------------------------------------------------------------- /ios/zowninative/Images-2.xcassets/Brand Assets.launchimage/iPhone SE Lounch Screen@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/delfrrr/weather-app-react-native/b0837e82ad96f76a9a52da306e4a6f9f21f32825/ios/zowninative/Images-2.xcassets/Brand Assets.launchimage/iPhone SE Lounch Screen@2x.png -------------------------------------------------------------------------------- /ios/zowninative/Images-2.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } -------------------------------------------------------------------------------- /ios/zowninative/Images.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "iphone", 5 | "size" : "20x20", 6 | "scale" : "2x" 7 | }, 8 | { 9 | "idiom" : "iphone", 10 | "size" : "20x20", 11 | "scale" : "3x" 12 | }, 13 | { 14 | "idiom" : "iphone", 15 | "size" : "29x29", 16 | "scale" : "2x" 17 | }, 18 | { 19 | "idiom" : "iphone", 20 | "size" : "29x29", 21 | "scale" : "3x" 22 | }, 23 | { 24 | "idiom" : "iphone", 25 | "size" : "40x40", 26 | "scale" : "2x" 27 | }, 28 | { 29 | "idiom" : "iphone", 30 | "size" : "40x40", 31 | "scale" : "3x" 32 | }, 33 | { 34 | "size" : "60x60", 35 | "idiom" : "iphone", 36 | "filename" : "icon-60-stripes@2x.png", 37 | "scale" : "2x" 38 | }, 39 | { 40 | "size" : "60x60", 41 | "idiom" : "iphone", 42 | "filename" : "icon-60-stripes@3x.png", 43 | "scale" : "3x" 44 | }, 45 | { 46 | "size" : "1024x1024", 47 | "idiom" : "ios-marketing", 48 | "filename" : "icon-60-stripes-1024-2.png", 49 | "scale" : "1x" 50 | } 51 | ], 52 | "info" : { 53 | "version" : 1, 54 | "author" : "xcode" 55 | } 56 | } -------------------------------------------------------------------------------- /ios/zowninative/Images.xcassets/AppIcon.appiconset/icon-60-stripes-1024-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/delfrrr/weather-app-react-native/b0837e82ad96f76a9a52da306e4a6f9f21f32825/ios/zowninative/Images.xcassets/AppIcon.appiconset/icon-60-stripes-1024-2.png -------------------------------------------------------------------------------- /ios/zowninative/Images.xcassets/AppIcon.appiconset/icon-60-stripes@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/delfrrr/weather-app-react-native/b0837e82ad96f76a9a52da306e4a6f9f21f32825/ios/zowninative/Images.xcassets/AppIcon.appiconset/icon-60-stripes@2x.png -------------------------------------------------------------------------------- /ios/zowninative/Images.xcassets/AppIcon.appiconset/icon-60-stripes@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/delfrrr/weather-app-react-native/b0837e82ad96f76a9a52da306e4a6f9f21f32825/ios/zowninative/Images.xcassets/AppIcon.appiconset/icon-60-stripes@3x.png -------------------------------------------------------------------------------- /ios/zowninative/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleDisplayName 8 | Zowni 9 | CFBundleExecutable 10 | $(EXECUTABLE_NAME) 11 | CFBundleIdentifier 12 | $(PRODUCT_BUNDLE_IDENTIFIER) 13 | CFBundleInfoDictionaryVersion 14 | 6.0 15 | CFBundleName 16 | $(PRODUCT_NAME) 17 | CFBundlePackageType 18 | APPL 19 | CFBundleShortVersionString 20 | 1.1 21 | CFBundleSignature 22 | ???? 23 | CFBundleURLTypes 24 | 25 | 26 | CFBundleURLSchemes 27 | 28 | fb327227487644093 29 | 30 | 31 | 32 | CFBundleVersion 33 | 101 34 | FacebookAppID 35 | 327227487644093 36 | FacebookDisplayName 37 | Zowni 38 | ITSAppUsesNonExemptEncryption 39 | 40 | LSApplicationQueriesSchemes 41 | 42 | fbapi 43 | fb-messenger-api 44 | fbauth2 45 | fbshareextension 46 | 47 | LSRequiresIPhoneOS 48 | 49 | NSAppTransportSecurity 50 | 51 | NSExceptionDomains 52 | 53 | localhost 54 | 55 | NSTemporaryExceptionAllowsInsecureHTTPLoads 56 | 57 | 58 | 59 | 60 | NSLocationWhenInUseUsageDescription 61 | Your location will be used only once to display weather in your current city 62 | NSPhotoLibraryUsageDescription 63 | we need access to photos for some reason 64 | UILaunchStoryboardName 65 | LaunchScreen 66 | UIRequiredDeviceCapabilities 67 | 68 | armv7 69 | 70 | UIStatusBarStyle 71 | UIStatusBarStyleLightContent 72 | UISupportedInterfaceOrientations 73 | 74 | UIInterfaceOrientationPortrait 75 | 76 | UIViewControllerBasedStatusBarAppearance 77 | 78 | 79 | 80 | -------------------------------------------------------------------------------- /ios/zowninative/main.m: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2015-present, Facebook, Inc. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the BSD-style license found in the 6 | * LICENSE file in the root directory of this source tree. An additional grant 7 | * of patent rights can be found in the PATENTS file in the same directory. 8 | */ 9 | 10 | #import 11 | 12 | #import "AppDelegate.h" 13 | 14 | int main(int argc, char * argv[]) { 15 | @autoreleasepool { 16 | return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class])); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /ios/zowninativeTests/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | BNDL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | 1 23 | 24 | 25 | -------------------------------------------------------------------------------- /ios/zowninativeTests/zowninativeTests.m: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2015-present, Facebook, Inc. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the BSD-style license found in the 6 | * LICENSE file in the root directory of this source tree. An additional grant 7 | * of patent rights can be found in the PATENTS file in the same directory. 8 | */ 9 | 10 | #import 11 | #import 12 | 13 | #import "RCTLog.h" 14 | #import "RCTRootView.h" 15 | 16 | #define TIMEOUT_SECONDS 600 17 | #define TEXT_TO_LOOK_FOR @"Welcome to React Native!" 18 | 19 | @interface zowninativeTests : XCTestCase 20 | 21 | @end 22 | 23 | @implementation zowninativeTests 24 | 25 | - (BOOL)findSubviewInView:(UIView *)view matching:(BOOL(^)(UIView *view))test 26 | { 27 | if (test(view)) { 28 | return YES; 29 | } 30 | for (UIView *subview in [view subviews]) { 31 | if ([self findSubviewInView:subview matching:test]) { 32 | return YES; 33 | } 34 | } 35 | return NO; 36 | } 37 | 38 | - (void)testRendersWelcomeScreen 39 | { 40 | UIViewController *vc = [[[[UIApplication sharedApplication] delegate] window] rootViewController]; 41 | NSDate *date = [NSDate dateWithTimeIntervalSinceNow:TIMEOUT_SECONDS]; 42 | BOOL foundElement = NO; 43 | 44 | __block NSString *redboxError = nil; 45 | RCTSetLogFunction(^(RCTLogLevel level, RCTLogSource source, NSString *fileName, NSNumber *lineNumber, NSString *message) { 46 | if (level >= RCTLogLevelError) { 47 | redboxError = message; 48 | } 49 | }); 50 | 51 | while ([date timeIntervalSinceNow] > 0 && !foundElement && !redboxError) { 52 | [[NSRunLoop mainRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate dateWithTimeIntervalSinceNow:0.1]]; 53 | [[NSRunLoop mainRunLoop] runMode:NSRunLoopCommonModes beforeDate:[NSDate dateWithTimeIntervalSinceNow:0.1]]; 54 | 55 | foundElement = [self findSubviewInView:vc.view matching:^BOOL(UIView *view) { 56 | if ([view.accessibilityLabel isEqualToString:TEXT_TO_LOOK_FOR]) { 57 | return YES; 58 | } 59 | return NO; 60 | }]; 61 | } 62 | 63 | RCTSetLogFunction(RCTDefaultLogFunction); 64 | 65 | XCTAssertNil(redboxError, @"RedBox error: %@", redboxError); 66 | XCTAssertTrue(foundElement, @"Couldn't find element with text '%@' in %d seconds", TEXT_TO_LOOK_FOR, TIMEOUT_SECONDS); 67 | } 68 | 69 | 70 | @end 71 | -------------------------------------------------------------------------------- /lib/beaufort.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @module beaufort 3 | */ 4 | const {scaleLinear} = require('d3-scale'); 5 | 6 | module.exports = scaleLinear() 7 | .domain([0, .3, 1.6, 3.4, 5.5, 8, 10.8, 13.9, 17.2, 20.8]) // m/s 8 | .range([0, 1, 2, 3, 4, 5, 6, 7, 8, 9]); //beaufort 9 | -------------------------------------------------------------------------------- /lib/format-temperature.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @module format-temperature 3 | */ 4 | 5 | /** 6 | * @param {number} temperatureC 7 | * @param {string} temperatureFormat F or C 8 | * @param {String} [sufix=''] to balance sighn 9 | * @return {string} 10 | */ 11 | module.exports = function (temperatureC, temperatureFormat, sufix = '') { 12 | if (Number.isNaN(temperatureC)) { 13 | return ''; 14 | } 15 | let temperature = Math.round(temperatureFormat === 'F' ? 16 | temperatureC * 1.8 + 32 : 17 | temperatureC); 18 | let temperatureStr = ''; 19 | if (temperature > 0) { 20 | temperatureStr = '+'; 21 | } else if (temperature < 0) { 22 | temperatureStr = '-'; 23 | } else { 24 | sufix = ''; //no sufix when no sighn 25 | } 26 | temperatureStr += String(Math.abs(temperature)) + sufix; 27 | return temperatureStr; 28 | } 29 | -------------------------------------------------------------------------------- /lib/getDataPoints.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @module lib/getDataPoints 3 | */ 4 | 5 | /** 6 | * checks points are consistent and return them 7 | * @param {ForecastDataBlock} dataBlock 8 | * @return {object[]|null} hours 9 | */ 10 | module.exports = function (dataBlock) { 11 | if ( 12 | dataBlock && 13 | dataBlock.data && 14 | dataBlock.data.length 15 | ) { 16 | let points = dataBlock.data.slice(0); 17 | //sitch on winter time 18 | if (points.length === 25) { 19 | points = points.slice(1); 20 | } 21 | //sitch on summer time 22 | if (points.length === 23) { 23 | points.unshift(points[0]); 24 | } 25 | const verfifiedPoints = points.filter( 26 | p => { 27 | return typeof p.apparentTemperature === 'number' && 28 | typeof p.temperature === 'number'; 29 | } 30 | ); 31 | if (verfifiedPoints.length === 24) { 32 | return verfifiedPoints; 33 | } 34 | } 35 | return null; 36 | } 37 | -------------------------------------------------------------------------------- /lib/getDimensions.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @module lib/getDimensions 3 | */ 4 | 5 | const {Dimensions} = require('react-native'); 6 | 7 | /** 8 | * @return {number} height without status bar 9 | */ 10 | module.exports = function () { 11 | const {height, width} = Dimensions.get('window'); 12 | return { 13 | width, height, 14 | viewHeight: height - 0, 15 | statusBarHeight: 20, 16 | curveHeight: height * .4 + 30 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /lib/getFeatureId.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @module lib/getFeatureId 3 | */ 4 | 5 | /** 6 | * @param {object} feature 7 | * @returns {string} 8 | */ 9 | module.exports = function getFeatureId(feature) { 10 | return feature.id || feature.properties.id; 11 | } 12 | -------------------------------------------------------------------------------- /lib/getFeatureLabel.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @module lib/getFeatureLabel 3 | */ 4 | 5 | /** 6 | * @param {Object} feature geocoder responce 7 | * @returns {String} result full name 8 | */ 9 | module.exports = function getFeatureLabel(feature) { 10 | if (feature && feature.place_name) { 11 | return feature.place_name; 12 | } 13 | if (feature && feature.properties && feature.properties.label) { 14 | return feature.properties.label; 15 | } 16 | return ''; 17 | } -------------------------------------------------------------------------------- /lib/reportError.js: -------------------------------------------------------------------------------- 1 | /** 2 | * show alert with error 3 | * @module reportError 4 | */ 5 | 6 | 'use strict'; 7 | 8 | const showAlert = require('./showAlert'); 9 | 10 | /** 11 | * use it insted of done 12 | * @param {Error} e 13 | */ 14 | module.exports = function reportError(e) { 15 | if (e.network) { 16 | showAlert( 17 | 'Looks like you are offline', 18 | 'Zowni needs Interenet connection.' + 19 | '\nTry again, when you have one.' 20 | ); 21 | } else { 22 | showAlert( 23 | 'Some problem with my code', 24 | e.message + 25 | '.\nMaybe, try again slowly.' 26 | ); 27 | } 28 | console.error(e); 29 | } 30 | -------------------------------------------------------------------------------- /lib/setStateAnimated.js: -------------------------------------------------------------------------------- 1 | /** 2 | * factory for setState animated method 3 | * @module setStateAnimated 4 | */ 5 | 6 | const {interpolate} = require('d3-interpolate'); 7 | const {isEqual} = require('lodash'); 8 | /** 9 | * @param {Number} [duration=500] 10 | * @param {Function} cb will be called before last set state 11 | * @return {function} set state method 12 | */ 13 | module.exports = function (duration = 500, cb) { 14 | let animation; 15 | let goingToState = null; 16 | return function (newState, lcb) { 17 | if (isEqual(goingToState, newState)) { 18 | return; 19 | } else { 20 | goingToState = newState; 21 | } 22 | const start = Date.now(); 23 | const interpolator = interpolate( 24 | this.state, 25 | newState 26 | ); 27 | animation = () => { 28 | const now = Date.now(); 29 | let t = (now - start) / duration; 30 | if (t > 1) { 31 | t = 1; 32 | } 33 | if (t === 1) { 34 | cb && cb(); 35 | lcb && lcb(); 36 | } 37 | this.setState(interpolator(t)); 38 | if (t < 1) { 39 | requestAnimationFrame(animation); 40 | } else { 41 | goingToState = null; 42 | } 43 | } 44 | requestAnimationFrame(animation); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /lib/showAlert.js: -------------------------------------------------------------------------------- 1 | /** 2 | * show alert with error 3 | * @module reportError 4 | */ 5 | 6 | 'use strict'; 7 | 8 | const {AlertIOS} = require('react-native'); 9 | let doNotOpen = false; 10 | 11 | /** 12 | * debounced alert 13 | * @param {string} title 14 | * @param {string} text 15 | */ 16 | module.exports = function showAlert(title, text) { 17 | if (doNotOpen) { 18 | return; 19 | } 20 | doNotOpen = true; 21 | AlertIOS.alert( 22 | title, 23 | text, 24 | function () { 25 | doNotOpen = false; 26 | } 27 | ); 28 | } 29 | -------------------------------------------------------------------------------- /lib/sliceDataPoints.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @module lib/sliceDataPoints 3 | */ 4 | 5 | 'use strict'; 6 | 7 | const getDataPoints = require('../lib/getDataPoints'); 8 | const moment = require('moment'); 9 | 10 | /** 11 | * slice data for current date 12 | * @param {ForecastDataBlock} hourly 13 | * @param {Date} date 14 | * @param {String} timezone 15 | * @param {Object} [current] current data point; when provided will be injected 16 | * @return {Object[]} 17 | */ 18 | module.exports = function (hourly, date, timezone, current) { 19 | const points = getDataPoints(hourly); 20 | if (!points) { 21 | return []; 22 | } 23 | let startTS = moment.tz(date, timezone).startOf('day').unix(); 24 | let endTS = startTS + 24 * 3600; 25 | let filteredPoints = points.filter(function ({time}) { 26 | return time >= startTS && time < endTS; 27 | }); 28 | if (current) { 29 | let currentTime = Math.floor(Date.now() / 1000); 30 | filteredPoints.forEach((dp, key) => { 31 | let prev = filteredPoints[key - 1]; 32 | if ( 33 | prev && 34 | prev.time <= currentTime && 35 | dp.time > currentTime 36 | ) { 37 | filteredPoints[key - 1] = Object.assign( 38 | {}, 39 | current, 40 | {current: true} 41 | ) 42 | } 43 | }) 44 | } 45 | return filteredPoints; 46 | } 47 | -------------------------------------------------------------------------------- /lib/temperature-color.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @module temperature-color 3 | */ 4 | 'use strict'; 5 | 6 | let scale = require('d3-scale').scaleLinear; 7 | 8 | /** 9 | * @param {number} temperature 10 | * @returns {string} color 11 | */ 12 | module.exports = scale().domain([ 13 | -Infinity, 14 | -20, 15 | -10, 16 | 0, 17 | 10, 18 | 20, 19 | 25, 20 | 30, 21 | 40, 22 | Infinity 23 | ]).range([ 24 | 'rgb(50, 50, 130)', 25 | 'rgb(50, 50, 130)', 26 | '#334DA3', 27 | '#3599dd', 28 | '#00A69E', 29 | '#54b87b', 30 | '#FFA15A', 31 | '#ff5053', 32 | 'rgb(236, 0, 125)', 33 | 'rgb(236, 0, 125)' 34 | ]); 35 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "zowninative", 3 | "version": "0.0.1", 4 | "private": true, 5 | "scripts": { 6 | "start": "node node_modules/react-native/local-cli/cli.js start" 7 | }, 8 | "license": "GPL-3.0", 9 | "dependencies": { 10 | "d3-interpolate": "1.1.1", 11 | "d3-path": "1.0.3", 12 | "d3-scale": "1.0.2", 13 | "d3-shape": "1.0.4", 14 | "lodash": "4.13.1", 15 | "moment": "2.13.0", 16 | "moment-timezone": "0.5.4", 17 | "react": "16.0.0-alpha.6", 18 | "react-native": "0.44.0", 19 | "react-native-blur": "3.0.0-alpha", 20 | "react-native-fbsdk": "0.5.0", 21 | "react-native-in-app-utils": "5.2.3", 22 | "react-native-locale": "0.0.16", 23 | "react-native-svg": "5.1.8", 24 | "react-redux": "4.4.5", 25 | "redux": "3.6.0", 26 | "url": "0.11.0" 27 | }, 28 | "devDependencies": { 29 | "eslint": "^2.10.2" 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /reducers/action-types.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @module action-types 3 | */ 4 | 5 | const actions = [ 6 | 'SET_POSITION', 7 | 'SET_LOCALITY', 8 | 'CHANGE_DATE', //opens date picker 9 | 'CLOSE_DATE_SELECT', 10 | 'SET_DATE', 11 | 'SET_TIMEZONE', 12 | 'SET_CURRENT_WEATHER', 13 | 'SET_HOURLY_WEATHER', 14 | 'SET_MIN_TEMPERATURE', 15 | 'SET_MAX_TEMPERATURE', 16 | 'SET_TEMPERATURE_FORMAT', 17 | 'TOGGLE_BAR', 18 | 'RESET_BAR', 19 | 'SET_HOUR_RANGE', 20 | 'TOGGLE_CITY_SELECT', 21 | 'SET_CITY_SEARCH_RESULT', 22 | 'LOAD_STATE_FROM_STORAGE', 23 | 'SET_SELECTED_LOCALITIES', 24 | 'ADD_SELECTED_LOCALITY', 25 | 'SET_LOCALITIES', 26 | 'ADD_LOCALITY', 27 | 'REMOVE_LOCALITY', 28 | 'FORECAST_REQUEST', 29 | 'SET_FORECAST_REQ', 30 | 'SET_CHART_FORMAT', 31 | 'OPEN_DETAILS', 32 | 'CLOSE_DETAILS', 33 | 'TOGGLE_CITY_SEARCH', 34 | 'SET_UNIT_SYSTEM', 35 | 'SET_12H', 36 | 'RAISE_FORECAST_API_LIMIT', 37 | 'SET_SHOW_STORE', 38 | 'SET_PRODUCTS', 39 | 'SET_FORECAST_API_LIMIT' 40 | ]; 41 | 42 | /** 43 | * @enum {string} 44 | */ 45 | module.exports = actions.reduce((actionTypes, action) => { 46 | actionTypes[action] = action; 47 | return actionTypes; 48 | }, {}); 49 | -------------------------------------------------------------------------------- /sketch/iPhone SE Lounch Screen@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/delfrrr/weather-app-react-native/b0837e82ad96f76a9a52da306e4a6f9f21f32825/sketch/iPhone SE Lounch Screen@1x.png -------------------------------------------------------------------------------- /sketch/iPhone SE Lounch Screen@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/delfrrr/weather-app-react-native/b0837e82ad96f76a9a52da306e4a6f9f21f32825/sketch/iPhone SE Lounch Screen@2x.png -------------------------------------------------------------------------------- /sketch/iPhone SE Lounch Screen@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/delfrrr/weather-app-react-native/b0837e82ad96f76a9a52da306e4a6f9f21f32825/sketch/iPhone SE Lounch Screen@3x.png -------------------------------------------------------------------------------- /sketch/icon-60-stripes-1024-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/delfrrr/weather-app-react-native/b0837e82ad96f76a9a52da306e4a6f9f21f32825/sketch/icon-60-stripes-1024-2.png -------------------------------------------------------------------------------- /sketch/icon-60-stripes-1024.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/delfrrr/weather-app-react-native/b0837e82ad96f76a9a52da306e4a6f9f21f32825/sketch/icon-60-stripes-1024.png -------------------------------------------------------------------------------- /sketch/icon-60-stripes.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/delfrrr/weather-app-react-native/b0837e82ad96f76a9a52da306e4a6f9f21f32825/sketch/icon-60-stripes.png -------------------------------------------------------------------------------- /sketch/icon-60-stripes@17,07x.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/delfrrr/weather-app-react-native/b0837e82ad96f76a9a52da306e4a6f9f21f32825/sketch/icon-60-stripes@17,07x.jpg -------------------------------------------------------------------------------- /sketch/icon-60-stripes@17,1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/delfrrr/weather-app-react-native/b0837e82ad96f76a9a52da306e4a6f9f21f32825/sketch/icon-60-stripes@17,1x.png -------------------------------------------------------------------------------- /sketch/icon-60-stripes@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/delfrrr/weather-app-react-native/b0837e82ad96f76a9a52da306e4a6f9f21f32825/sketch/icon-60-stripes@1x.png -------------------------------------------------------------------------------- /sketch/icon-60-stripes@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/delfrrr/weather-app-react-native/b0837e82ad96f76a9a52da306e4a6f9f21f32825/sketch/icon-60-stripes@2x.png -------------------------------------------------------------------------------- /sketch/icon-60-stripes@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/delfrrr/weather-app-react-native/b0837e82ad96f76a9a52da306e4a6f9f21f32825/sketch/icon-60-stripes@3x.png -------------------------------------------------------------------------------- /sketch/icon.sketch: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/delfrrr/weather-app-react-native/b0837e82ad96f76a9a52da306e4a6f9f21f32825/sketch/icon.sketch -------------------------------------------------------------------------------- /sketch/icon@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/delfrrr/weather-app-react-native/b0837e82ad96f76a9a52da306e4a6f9f21f32825/sketch/icon@2x.png -------------------------------------------------------------------------------- /sketch/icon@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/delfrrr/weather-app-react-native/b0837e82ad96f76a9a52da306e4a6f9f21f32825/sketch/icon@3x.png -------------------------------------------------------------------------------- /sketch/layouts.sketch: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/delfrrr/weather-app-react-native/b0837e82ad96f76a9a52da306e4a6f9f21f32825/sketch/layouts.sketch -------------------------------------------------------------------------------- /sketch/locaion.svg: -------------------------------------------------------------------------------- 1 | locaion -------------------------------------------------------------------------------- /sketch/next day.svg: -------------------------------------------------------------------------------- 1 | next day -------------------------------------------------------------------------------- /sketch/next menu level.svg: -------------------------------------------------------------------------------- 1 | next menu level -------------------------------------------------------------------------------- /sketch/prev day copy.svg: -------------------------------------------------------------------------------- 1 | prev day copy --------------------------------------------------------------------------------