From 3a60a35a9945d2e6cb61fa9aa1bfa61ff38bd7c5 Mon Sep 17 00:00:00 2001 From: Tom Hacohen Date: Thu, 30 Nov 2017 11:06:15 +0000 Subject: [PATCH 001/912] Initial commit. --- .gitignore | 21 + README.md | 2164 +++++++++++++ package.json | 22 + public/favicon.ico | Bin 0 -> 3870 bytes public/index.html | 40 + public/manifest.json | 15 + src/App.css | 24 + src/App.test.tsx | 8 + src/App.tsx | 22 + src/index.css | 5 + src/index.tsx | 11 + src/logo.svg | 7 + src/registerServiceWorker.ts | 114 + tsconfig.json | 29 + tsconfig.test.json | 6 + tslint.json | 99 + yarn.lock | 5809 ++++++++++++++++++++++++++++++++++ 17 files changed, 8396 insertions(+) create mode 100644 .gitignore create mode 100644 README.md create mode 100644 package.json create mode 100644 public/favicon.ico create mode 100644 public/index.html create mode 100644 public/manifest.json create mode 100644 src/App.css create mode 100644 src/App.test.tsx create mode 100644 src/App.tsx create mode 100644 src/index.css create mode 100644 src/index.tsx create mode 100644 src/logo.svg create mode 100644 src/registerServiceWorker.ts create mode 100644 tsconfig.json create mode 100644 tsconfig.test.json create mode 100644 tslint.json create mode 100644 yarn.lock diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..d30f40e --- /dev/null +++ b/.gitignore @@ -0,0 +1,21 @@ +# See https://help.github.com/ignore-files/ for more about ignoring files. + +# dependencies +/node_modules + +# testing +/coverage + +# production +/build + +# misc +.DS_Store +.env.local +.env.development.local +.env.test.local +.env.production.local + +npm-debug.log* +yarn-debug.log* +yarn-error.log* diff --git a/README.md b/README.md new file mode 100644 index 0000000..c55ccdf --- /dev/null +++ b/README.md @@ -0,0 +1,2164 @@ +This project was bootstrapped with [Create React App](https://github.com/facebookincubator/create-react-app). + +Below you will find some information on how to perform common tasks.
+You can find the most recent version of this guide [here](https://github.com/facebookincubator/create-react-app/blob/master/packages/react-scripts/template/README.md). + +## Table of Contents + +- [Updating to New Releases](#updating-to-new-releases) +- [Sending Feedback](#sending-feedback) +- [Folder Structure](#folder-structure) +- [Available Scripts](#available-scripts) + - [npm start](#npm-start) + - [npm test](#npm-test) + - [npm run build](#npm-run-build) + - [npm run eject](#npm-run-eject) +- [Supported Language Features and Polyfills](#supported-language-features-and-polyfills) +- [Syntax Highlighting in the Editor](#syntax-highlighting-in-the-editor) +- [Displaying Lint Output in the Editor](#displaying-lint-output-in-the-editor) +- [Debugging in the Editor](#debugging-in-the-editor) +- [Formatting Code Automatically](#formatting-code-automatically) +- [Changing the Page ``](#changing-the-page-title) +- [Installing a Dependency](#installing-a-dependency) +- [Importing a Component](#importing-a-component) +- [Code Splitting](#code-splitting) +- [Adding a Stylesheet](#adding-a-stylesheet) +- [Post-Processing CSS](#post-processing-css) +- [Adding a CSS Preprocessor (Sass, Less etc.)](#adding-a-css-preprocessor-sass-less-etc) +- [Adding Images, Fonts, and Files](#adding-images-fonts-and-files) +- [Using the `public` Folder](#using-the-public-folder) + - [Changing the HTML](#changing-the-html) + - [Adding Assets Outside of the Module System](#adding-assets-outside-of-the-module-system) + - [When to Use the `public` Folder](#when-to-use-the-public-folder) +- [Using Global Variables](#using-global-variables) +- [Adding Bootstrap](#adding-bootstrap) + - [Using a Custom Theme](#using-a-custom-theme) +- [Adding Flow](#adding-flow) +- [Adding Custom Environment Variables](#adding-custom-environment-variables) + - [Referencing Environment Variables in the HTML](#referencing-environment-variables-in-the-html) + - [Adding Temporary Environment Variables In Your Shell](#adding-temporary-environment-variables-in-your-shell) + - [Adding Development Environment Variables In `.env`](#adding-development-environment-variables-in-env) +- [Can I Use Decorators?](#can-i-use-decorators) +- [Integrating with an API Backend](#integrating-with-an-api-backend) + - [Node](#node) + - [Ruby on Rails](#ruby-on-rails) +- [Proxying API Requests in Development](#proxying-api-requests-in-development) + - ["Invalid Host Header" Errors After Configuring Proxy](#invalid-host-header-errors-after-configuring-proxy) + - [Configuring the Proxy Manually](#configuring-the-proxy-manually) + - [Configuring a WebSocket Proxy](#configuring-a-websocket-proxy) +- [Using HTTPS in Development](#using-https-in-development) +- [Generating Dynamic `<meta>` Tags on the Server](#generating-dynamic-meta-tags-on-the-server) +- [Pre-Rendering into Static HTML Files](#pre-rendering-into-static-html-files) +- [Injecting Data from the Server into the Page](#injecting-data-from-the-server-into-the-page) +- [Running Tests](#running-tests) + - [Filename Conventions](#filename-conventions) + - [Command Line Interface](#command-line-interface) + - [Version Control Integration](#version-control-integration) + - [Writing Tests](#writing-tests) + - [Testing Components](#testing-components) + - [Using Third Party Assertion Libraries](#using-third-party-assertion-libraries) + - [Initializing Test Environment](#initializing-test-environment) + - [Focusing and Excluding Tests](#focusing-and-excluding-tests) + - [Coverage Reporting](#coverage-reporting) + - [Continuous Integration](#continuous-integration) + - [Disabling jsdom](#disabling-jsdom) + - [Snapshot Testing](#snapshot-testing) + - [Editor Integration](#editor-integration) +- [Developing Components in Isolation](#developing-components-in-isolation) + - [Getting Started with Storybook](#getting-started-with-storybook) + - [Getting Started with Styleguidist](#getting-started-with-styleguidist) +- [Making a Progressive Web App](#making-a-progressive-web-app) + - [Opting Out of Caching](#opting-out-of-caching) + - [Offline-First Considerations](#offline-first-considerations) + - [Progressive Web App Metadata](#progressive-web-app-metadata) +- [Analyzing the Bundle Size](#analyzing-the-bundle-size) +- [Deployment](#deployment) + - [Static Server](#static-server) + - [Other Solutions](#other-solutions) + - [Serving Apps with Client-Side Routing](#serving-apps-with-client-side-routing) + - [Building for Relative Paths](#building-for-relative-paths) + - [Azure](#azure) + - [Firebase](#firebase) + - [GitHub Pages](#github-pages) + - [Heroku](#heroku) + - [Netlify](#netlify) + - [Now](#now) + - [S3 and CloudFront](#s3-and-cloudfront) + - [Surge](#surge) +- [Advanced Configuration](#advanced-configuration) +- [Troubleshooting](#troubleshooting) + - [`npm start` doesn’t detect changes](#npm-start-doesnt-detect-changes) + - [`npm test` hangs on macOS Sierra](#npm-test-hangs-on-macos-sierra) + - [`npm run build` exits too early](#npm-run-build-exits-too-early) + - [`npm run build` fails on Heroku](#npm-run-build-fails-on-heroku) + - [`npm run build` fails to minify](#npm-run-build-fails-to-minify) + - [Moment.js locales are missing](#momentjs-locales-are-missing) +- [Something Missing?](#something-missing) + +## Updating to New Releases + +Create React App is divided into two packages: + +* `create-react-app` is a global command-line utility that you use to create new projects. +* `react-scripts` is a development dependency in the generated projects (including this one). + +You almost never need to update `create-react-app` itself: it delegates all the setup to `react-scripts`. + +When you run `create-react-app`, it always creates the project with the latest version of `react-scripts` so you’ll get all the new features and improvements in newly created apps automatically. + +To update an existing project to a new version of `react-scripts`, [open the changelog](https://github.com/facebookincubator/create-react-app/blob/master/CHANGELOG.md), find the version you’re currently on (check `package.json` in this folder if you’re not sure), and apply the migration instructions for the newer versions. + +In most cases bumping the `react-scripts` version in `package.json` and running `npm install` in this folder should be enough, but it’s good to consult the [changelog](https://github.com/facebookincubator/create-react-app/blob/master/CHANGELOG.md) for potential breaking changes. + +We commit to keeping the breaking changes minimal so you can upgrade `react-scripts` painlessly. + +## Sending Feedback + +We are always open to [your feedback](https://github.com/facebookincubator/create-react-app/issues). + +## Folder Structure + +After creation, your project should look like this: + +``` +my-app/ + README.md + node_modules/ + package.json + public/ + index.html + favicon.ico + src/ + App.css + App.js + App.test.js + index.css + index.js + logo.svg +``` + +For the project to build, **these files must exist with exact filenames**: + +* `public/index.html` is the page template; +* `src/index.js` is the JavaScript entry point. + +You can delete or rename the other files. + +You may create subdirectories inside `src`. For faster rebuilds, only files inside `src` are processed by Webpack.<br> +You need to **put any JS and CSS files inside `src`**, otherwise Webpack won’t see them. + +Only files inside `public` can be used from `public/index.html`.<br> +Read instructions below for using assets from JavaScript and HTML. + +You can, however, create more top-level directories.<br> +They will not be included in the production build so you can use them for things like documentation. + +## Available Scripts + +In the project directory, you can run: + +### `npm start` + +Runs the app in the development mode.<br> +Open [http://localhost:3000](http://localhost:3000) to view it in the browser. + +The page will reload if you make edits.<br> +You will also see any lint errors in the console. + +### `npm test` + +Launches the test runner in the interactive watch mode.<br> +See the section about [running tests](#running-tests) for more information. + +### `npm run build` + +Builds the app for production to the `build` folder.<br> +It correctly bundles React in production mode and optimizes the build for the best performance. + +The build is minified and the filenames include the hashes.<br> +Your app is ready to be deployed! + +See the section about [deployment](#deployment) for more information. + +### `npm run eject` + +**Note: this is a one-way operation. Once you `eject`, you can’t go back!** + +If you aren’t satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project. + +Instead, it will copy all the configuration files and the transitive dependencies (Webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you’re on your own. + +You don’t have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn’t feel obligated to use this feature. However we understand that this tool wouldn’t be useful if you couldn’t customize it when you are ready for it. + +## Supported Language Features and Polyfills + +This project supports a superset of the latest JavaScript standard.<br> +In addition to [ES6](https://github.com/lukehoban/es6features) syntax features, it also supports: + +* [Exponentiation Operator](https://github.com/rwaldron/exponentiation-operator) (ES2016). +* [Async/await](https://github.com/tc39/ecmascript-asyncawait) (ES2017). +* [Object Rest/Spread Properties](https://github.com/sebmarkbage/ecmascript-rest-spread) (stage 3 proposal). +* [Dynamic import()](https://github.com/tc39/proposal-dynamic-import) (stage 3 proposal) +* [Class Fields and Static Properties](https://github.com/tc39/proposal-class-public-fields) (stage 2 proposal). +* [JSX](https://facebook.github.io/react/docs/introducing-jsx.html) and [Flow](https://flowtype.org/) syntax. + +Learn more about [different proposal stages](https://babeljs.io/docs/plugins/#presets-stage-x-experimental-presets-). + +While we recommend to use experimental proposals with some caution, Facebook heavily uses these features in the product code, so we intend to provide [codemods](https://medium.com/@cpojer/effective-javascript-codemods-5a6686bb46fb) if any of these proposals change in the future. + +Note that **the project only includes a few ES6 [polyfills](https://en.wikipedia.org/wiki/Polyfill)**: + +* [`Object.assign()`](https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Global_Objects/Object/assign) via [`object-assign`](https://github.com/sindresorhus/object-assign). +* [`Promise`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise) via [`promise`](https://github.com/then/promise). +* [`fetch()`](https://developer.mozilla.org/en/docs/Web/API/Fetch_API) via [`whatwg-fetch`](https://github.com/github/fetch). + +If you use any other ES6+ features that need **runtime support** (such as `Array.from()` or `Symbol`), make sure you are including the appropriate polyfills manually, or that the browsers you are targeting already support them. + +## Syntax Highlighting in the Editor + +To configure the syntax highlighting in your favorite text editor, head to the [relevant Babel documentation page](https://babeljs.io/docs/editors) and follow the instructions. Some of the most popular editors are covered. + +## Displaying Lint Output in the Editor + +>Note: this feature is available with `react-scripts@0.2.0` and higher.<br> +>It also only works with npm 3 or higher. + +Some editors, including Sublime Text, Atom, and Visual Studio Code, provide plugins for ESLint. + +They are not required for linting. You should see the linter output right in your terminal as well as the browser console. However, if you prefer the lint results to appear right in your editor, there are some extra steps you can do. + +You would need to install an ESLint plugin for your editor first. Then, add a file called `.eslintrc` to the project root: + +```js +{ + "extends": "react-app" +} +``` + +Now your editor should report the linting warnings. + +Note that even if you edit your `.eslintrc` file further, these changes will **only affect the editor integration**. They won’t affect the terminal and in-browser lint output. This is because Create React App intentionally provides a minimal set of rules that find common mistakes. + +If you want to enforce a coding style for your project, consider using [Prettier](https://github.com/jlongster/prettier) instead of ESLint style rules. + +## Debugging in the Editor + +**This feature is currently only supported by [Visual Studio Code](https://code.visualstudio.com) and [WebStorm](https://www.jetbrains.com/webstorm/).** + +Visual Studio Code and WebStorm support debugging out of the box with Create React App. This enables you as a developer to write and debug your React code without leaving the editor, and most importantly it enables you to have a continuous development workflow, where context switching is minimal, as you don’t have to switch between tools. + +### Visual Studio Code + +You would need to have the latest version of [VS Code](https://code.visualstudio.com) and VS Code [Chrome Debugger Extension](https://marketplace.visualstudio.com/items?itemName=msjsdiag.debugger-for-chrome) installed. + +Then add the block below to your `launch.json` file and put it inside the `.vscode` folder in your app’s root directory. + +```json +{ + "version": "0.2.0", + "configurations": [{ + "name": "Chrome", + "type": "chrome", + "request": "launch", + "url": "http://localhost:3000", + "webRoot": "${workspaceRoot}/src", + "userDataDir": "${workspaceRoot}/.vscode/chrome", + "sourceMapPathOverrides": { + "webpack:///src/*": "${webRoot}/*" + } + }] +} +``` +>Note: the URL may be different if you've made adjustments via the [HOST or PORT environment variables](#advanced-configuration). + +Start your app by running `npm start`, and start debugging in VS Code by pressing `F5` or by clicking the green debug icon. You can now write code, set breakpoints, make changes to the code, and debug your newly modified code—all from your editor. + +### WebStorm + +You would need to have [WebStorm](https://www.jetbrains.com/webstorm/) and [JetBrains IDE Support](https://chrome.google.com/webstore/detail/jetbrains-ide-support/hmhgeddbohgjknpmjagkdomcpobmllji) Chrome extension installed. + +In the WebStorm menu `Run` select `Edit Configurations...`. Then click `+` and select `JavaScript Debug`. Paste `http://localhost:3000` into the URL field and save the configuration. + +>Note: the URL may be different if you've made adjustments via the [HOST or PORT environment variables](#advanced-configuration). + +Start your app by running `npm start`, then press `^D` on macOS or `F9` on Windows and Linux or click the green debug icon to start debugging in WebStorm. + +The same way you can debug your application in IntelliJ IDEA Ultimate, PhpStorm, PyCharm Pro, and RubyMine. + +## Formatting Code Automatically + +Prettier is an opinionated code formatter with support for JavaScript, CSS and JSON. With Prettier you can format the code you write automatically to ensure a code style within your project. See the [Prettier's GitHub page](https://github.com/prettier/prettier) for more information, and look at this [page to see it in action](https://prettier.github.io/prettier/). + +To format our code whenever we make a commit in git, we need to install the following dependencies: + +```sh +npm install --save husky lint-staged prettier +``` + +Alternatively you may use `yarn`: + +```sh +yarn add husky lint-staged prettier +``` + +* `husky` makes it easy to use githooks as if they are npm scripts. +* `lint-staged` allows us to run scripts on staged files in git. See this [blog post about lint-staged to learn more about it](https://medium.com/@okonetchnikov/make-linting-great-again-f3890e1ad6b8). +* `prettier` is the JavaScript formatter we will run before commits. + +Now we can make sure every file is formatted correctly by adding a few lines to the `package.json` in the project root. + +Add the following line to `scripts` section: + +```diff + "scripts": { ++ "precommit": "lint-staged", + "start": "react-scripts start", + "build": "react-scripts build", +``` + +Next we add a 'lint-staged' field to the `package.json`, for example: + +```diff + "dependencies": { + // ... + }, ++ "lint-staged": { ++ "src/**/*.{js,jsx,json,css}": [ ++ "prettier --single-quote --write", ++ "git add" ++ ] ++ }, + "scripts": { +``` + +Now, whenever you make a commit, Prettier will format the changed files automatically. You can also run `./node_modules/.bin/prettier --single-quote --write "src/**/*.{js,jsx}"` to format your entire project for the first time. + +Next you might want to integrate Prettier in your favorite editor. Read the section on [Editor Integration](https://github.com/prettier/prettier#editor-integration) on the Prettier GitHub page. + +## Changing the Page `<title>` + +You can find the source HTML file in the `public` folder of the generated project. You may edit the `<title>` tag in it to change the title from “React App” to anything else. + +Note that normally you wouldn’t edit files in the `public` folder very often. For example, [adding a stylesheet](#adding-a-stylesheet) is done without touching the HTML. + +If you need to dynamically update the page title based on the content, you can use the browser [`document.title`](https://developer.mozilla.org/en-US/docs/Web/API/Document/title) API. For more complex scenarios when you want to change the title from React components, you can use [React Helmet](https://github.com/nfl/react-helmet), a third party library. + +If you use a custom server for your app in production and want to modify the title before it gets sent to the browser, you can follow advice in [this section](#generating-dynamic-meta-tags-on-the-server). Alternatively, you can pre-build each page as a static HTML file which then loads the JavaScript bundle, which is covered [here](#pre-rendering-into-static-html-files). + +## Installing a Dependency + +The generated project includes React and ReactDOM as dependencies. It also includes a set of scripts used by Create React App as a development dependency. You may install other dependencies (for example, React Router) with `npm`: + +```sh +npm install --save react-router +``` + +Alternatively you may use `yarn`: + +```sh +yarn add react-router +``` + +This works for any library, not just `react-router`. + +## Importing a Component + +This project setup supports ES6 modules thanks to Babel.<br> +While you can still use `require()` and `module.exports`, we encourage you to use [`import` and `export`](http://exploringjs.com/es6/ch_modules.html) instead. + +For example: + +### `Button.js` + +```js +import React, { Component } from 'react'; + +class Button extends Component { + render() { + // ... + } +} + +export default Button; // Don’t forget to use export default! +``` + +### `DangerButton.js` + + +```js +import React, { Component } from 'react'; +import Button from './Button'; // Import a component from another file + +class DangerButton extends Component { + render() { + return <Button color="red" />; + } +} + +export default DangerButton; +``` + +Be aware of the [difference between default and named exports](http://stackoverflow.com/questions/36795819/react-native-es-6-when-should-i-use-curly-braces-for-import/36796281#36796281). It is a common source of mistakes. + +We suggest that you stick to using default imports and exports when a module only exports a single thing (for example, a component). That’s what you get when you use `export default Button` and `import Button from './Button'`. + +Named exports are useful for utility modules that export several functions. A module may have at most one default export and as many named exports as you like. + +Learn more about ES6 modules: + +* [When to use the curly braces?](http://stackoverflow.com/questions/36795819/react-native-es-6-when-should-i-use-curly-braces-for-import/36796281#36796281) +* [Exploring ES6: Modules](http://exploringjs.com/es6/ch_modules.html) +* [Understanding ES6: Modules](https://leanpub.com/understandinges6/read#leanpub-auto-encapsulating-code-with-modules) + +## Code Splitting + +Instead of downloading the entire app before users can use it, code splitting allows you to split your code into small chunks which you can then load on demand. + +This project setup supports code splitting via [dynamic `import()`](http://2ality.com/2017/01/import-operator.html#loading-code-on-demand). Its [proposal](https://github.com/tc39/proposal-dynamic-import) is in stage 3. The `import()` function-like form takes the module name as an argument and returns a [`Promise`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise) which always resolves to the namespace object of the module. + +Here is an example: + +### `moduleA.js` + +```js +const moduleA = 'Hello'; + +export { moduleA }; +``` +### `App.js` + +```js +import React, { Component } from 'react'; + +class App extends Component { + handleClick = () => { + import('./moduleA') + .then(({ moduleA }) => { + // Use moduleA + }) + .catch(err => { + // Handle failure + }); + }; + + render() { + return ( + <div> + <button onClick={this.handleClick}>Load</button> + </div> + ); + } +} + +export default App; +``` + +This will make `moduleA.js` and all its unique dependencies as a separate chunk that only loads after the user clicks the 'Load' button. + +You can also use it with `async` / `await` syntax if you prefer it. + +### With React Router + +If you are using React Router check out [this tutorial](http://serverless-stack.com/chapters/code-splitting-in-create-react-app.html) on how to use code splitting with it. You can find the companion GitHub repository [here](https://github.com/AnomalyInnovations/serverless-stack-demo-client/tree/code-splitting-in-create-react-app). + +## Adding a Stylesheet + +This project setup uses [Webpack](https://webpack.js.org/) for handling all assets. Webpack offers a custom way of “extending” the concept of `import` beyond JavaScript. To express that a JavaScript file depends on a CSS file, you need to **import the CSS from the JavaScript file**: + +### `Button.css` + +```css +.Button { + padding: 20px; +} +``` + +### `Button.js` + +```js +import React, { Component } from 'react'; +import './Button.css'; // Tell Webpack that Button.js uses these styles + +class Button extends Component { + render() { + // You can use them as regular CSS styles + return <div className="Button" />; + } +} +``` + +**This is not required for React** but many people find this feature convenient. You can read about the benefits of this approach [here](https://medium.com/seek-ui-engineering/block-element-modifying-your-javascript-components-d7f99fcab52b). However you should be aware that this makes your code less portable to other build tools and environments than Webpack. + +In development, expressing dependencies this way allows your styles to be reloaded on the fly as you edit them. In production, all CSS files will be concatenated into a single minified `.css` file in the build output. + +If you are concerned about using Webpack-specific semantics, you can put all your CSS right into `src/index.css`. It would still be imported from `src/index.js`, but you could always remove that import if you later migrate to a different build tool. + +## Post-Processing CSS + +This project setup minifies your CSS and adds vendor prefixes to it automatically through [Autoprefixer](https://github.com/postcss/autoprefixer) so you don’t need to worry about it. + +For example, this: + +```css +.App { + display: flex; + flex-direction: row; + align-items: center; +} +``` + +becomes this: + +```css +.App { + display: -webkit-box; + display: -ms-flexbox; + display: flex; + -webkit-box-orient: horizontal; + -webkit-box-direction: normal; + -ms-flex-direction: row; + flex-direction: row; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; +} +``` + +If you need to disable autoprefixing for some reason, [follow this section](https://github.com/postcss/autoprefixer#disabling). + +## Adding a CSS Preprocessor (Sass, Less etc.) + +Generally, we recommend that you don’t reuse the same CSS classes across different components. For example, instead of using a `.Button` CSS class in `<AcceptButton>` and `<RejectButton>` components, we recommend creating a `<Button>` component with its own `.Button` styles, that both `<AcceptButton>` and `<RejectButton>` can render (but [not inherit](https://facebook.github.io/react/docs/composition-vs-inheritance.html)). + +Following this rule often makes CSS preprocessors less useful, as features like mixins and nesting are replaced by component composition. You can, however, integrate a CSS preprocessor if you find it valuable. In this walkthrough, we will be using Sass, but you can also use Less, or another alternative. + +First, let’s install the command-line interface for Sass: + +```sh +npm install --save node-sass-chokidar +``` + +Alternatively you may use `yarn`: + +```sh +yarn add node-sass-chokidar +``` + +Then in `package.json`, add the following lines to `scripts`: + +```diff + "scripts": { ++ "build-css": "node-sass-chokidar src/ -o src/", ++ "watch-css": "npm run build-css && node-sass-chokidar src/ -o src/ --watch --recursive", + "start": "react-scripts start", + "build": "react-scripts build", + "test": "react-scripts test --env=jsdom", +``` + +>Note: To use a different preprocessor, replace `build-css` and `watch-css` commands according to your preprocessor’s documentation. + +Now you can rename `src/App.css` to `src/App.scss` and run `npm run watch-css`. The watcher will find every Sass file in `src` subdirectories, and create a corresponding CSS file next to it, in our case overwriting `src/App.css`. Since `src/App.js` still imports `src/App.css`, the styles become a part of your application. You can now edit `src/App.scss`, and `src/App.css` will be regenerated. + +To share variables between Sass files, you can use Sass imports. For example, `src/App.scss` and other component style files could include `@import "./shared.scss";` with variable definitions. + +To enable importing files without using relative paths, you can add the `--include-path` option to the command in `package.json`. + +``` +"build-css": "node-sass-chokidar --include-path ./src --include-path ./node_modules src/ -o src/", +"watch-css": "npm run build-css && node-sass-chokidar --include-path ./src --include-path ./node_modules src/ -o src/ --watch --recursive", +``` + +This will allow you to do imports like + +```scss +@import 'styles/_colors.scss'; // assuming a styles directory under src/ +@import 'nprogress/nprogress'; // importing a css file from the nprogress node module +``` + +At this point you might want to remove all CSS files from the source control, and add `src/**/*.css` to your `.gitignore` file. It is generally a good practice to keep the build products outside of the source control. + +As a final step, you may find it convenient to run `watch-css` automatically with `npm start`, and run `build-css` as a part of `npm run build`. You can use the `&&` operator to execute two scripts sequentially. However, there is no cross-platform way to run two scripts in parallel, so we will install a package for this: + +```sh +npm install --save npm-run-all +``` + +Alternatively you may use `yarn`: + +```sh +yarn add npm-run-all +``` + +Then we can change `start` and `build` scripts to include the CSS preprocessor commands: + +```diff + "scripts": { + "build-css": "node-sass-chokidar src/ -o src/", + "watch-css": "npm run build-css && node-sass-chokidar src/ -o src/ --watch --recursive", +- "start": "react-scripts start", +- "build": "react-scripts build", ++ "start-js": "react-scripts start", ++ "start": "npm-run-all -p watch-css start-js", ++ "build": "npm run build-css && react-scripts build", + "test": "react-scripts test --env=jsdom", + "eject": "react-scripts eject" + } +``` + +Now running `npm start` and `npm run build` also builds Sass files. + +**Why `node-sass-chokidar`?** + +`node-sass` has been reported as having the following issues: + +- `node-sass --watch` has been reported to have *performance issues* in certain conditions when used in a virtual machine or with docker. + +- Infinite styles compiling [#1939](https://github.com/facebookincubator/create-react-app/issues/1939) + +- `node-sass` has been reported as having issues with detecting new files in a directory [#1891](https://github.com/sass/node-sass/issues/1891) + + `node-sass-chokidar` is used here as it addresses these issues. + +## Adding Images, Fonts, and Files + +With Webpack, using static assets like images and fonts works similarly to CSS. + +You can **`import` a file right in a JavaScript module**. This tells Webpack to include that file in the bundle. Unlike CSS imports, importing a file gives you a string value. This value is the final path you can reference in your code, e.g. as the `src` attribute of an image or the `href` of a link to a PDF. + +To reduce the number of requests to the server, importing images that are less than 10,000 bytes returns a [data URI](https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/Data_URIs) instead of a path. This applies to the following file extensions: bmp, gif, jpg, jpeg, and png. SVG files are excluded due to [#1153](https://github.com/facebookincubator/create-react-app/issues/1153). + +Here is an example: + +```js +import React from 'react'; +import logo from './logo.png'; // Tell Webpack this JS file uses this image + +console.log(logo); // /logo.84287d09.png + +function Header() { + // Import result is the URL of your image + return <img src={logo} alt="Logo" />; +} + +export default Header; +``` + +This ensures that when the project is built, Webpack will correctly move the images into the build folder, and provide us with correct paths. + +This works in CSS too: + +```css +.Logo { + background-image: url(./logo.png); +} +``` + +Webpack finds all relative module references in CSS (they start with `./`) and replaces them with the final paths from the compiled bundle. If you make a typo or accidentally delete an important file, you will see a compilation error, just like when you import a non-existent JavaScript module. The final filenames in the compiled bundle are generated by Webpack from content hashes. If the file content changes in the future, Webpack will give it a different name in production so you don’t need to worry about long-term caching of assets. + +Please be advised that this is also a custom feature of Webpack. + +**It is not required for React** but many people enjoy it (and React Native uses a similar mechanism for images).<br> +An alternative way of handling static assets is described in the next section. + +## Using the `public` Folder + +>Note: this feature is available with `react-scripts@0.5.0` and higher. + +### Changing the HTML + +The `public` folder contains the HTML file so you can tweak it, for example, to [set the page title](#changing-the-page-title). +The `<script>` tag with the compiled code will be added to it automatically during the build process. + +### Adding Assets Outside of the Module System + +You can also add other assets to the `public` folder. + +Note that we normally encourage you to `import` assets in JavaScript files instead. +For example, see the sections on [adding a stylesheet](#adding-a-stylesheet) and [adding images and fonts](#adding-images-fonts-and-files). +This mechanism provides a number of benefits: + +* Scripts and stylesheets get minified and bundled together to avoid extra network requests. +* Missing files cause compilation errors instead of 404 errors for your users. +* Result filenames include content hashes so you don’t need to worry about browsers caching their old versions. + +However there is an **escape hatch** that you can use to add an asset outside of the module system. + +If you put a file into the `public` folder, it will **not** be processed by Webpack. Instead it will be copied into the build folder untouched. To reference assets in the `public` folder, you need to use a special variable called `PUBLIC_URL`. + +Inside `index.html`, you can use it like this: + +```html +<link rel="shortcut icon" href="%PUBLIC_URL%/favicon.ico"> +``` + +Only files inside the `public` folder will be accessible by `%PUBLIC_URL%` prefix. If you need to use a file from `src` or `node_modules`, you’ll have to copy it there to explicitly specify your intention to make this file a part of the build. + +When you run `npm run build`, Create React App will substitute `%PUBLIC_URL%` with a correct absolute path so your project works even if you use client-side routing or host it at a non-root URL. + +In JavaScript code, you can use `process.env.PUBLIC_URL` for similar purposes: + +```js +render() { + // Note: this is an escape hatch and should be used sparingly! + // Normally we recommend using `import` for getting asset URLs + // as described in “Adding Images and Fonts” above this section. + return <img src={process.env.PUBLIC_URL + '/img/logo.png'} />; +} +``` + +Keep in mind the downsides of this approach: + +* None of the files in `public` folder get post-processed or minified. +* Missing files will not be called at compilation time, and will cause 404 errors for your users. +* Result filenames won’t include content hashes so you’ll need to add query arguments or rename them every time they change. + +### When to Use the `public` Folder + +Normally we recommend importing [stylesheets](#adding-a-stylesheet), [images, and fonts](#adding-images-fonts-and-files) from JavaScript. +The `public` folder is useful as a workaround for a number of less common cases: + +* You need a file with a specific name in the build output, such as [`manifest.webmanifest`](https://developer.mozilla.org/en-US/docs/Web/Manifest). +* You have thousands of images and need to dynamically reference their paths. +* You want to include a small script like [`pace.js`](http://github.hubspot.com/pace/docs/welcome/) outside of the bundled code. +* Some library may be incompatible with Webpack and you have no other option but to include it as a `<script>` tag. + +Note that if you add a `<script>` that declares global variables, you also need to read the next section on using them. + +## Using Global Variables + +When you include a script in the HTML file that defines global variables and try to use one of these variables in the code, the linter will complain because it cannot see the definition of the variable. + +You can avoid this by reading the global variable explicitly from the `window` object, for example: + +```js +const $ = window.$; +``` + +This makes it obvious you are using a global variable intentionally rather than because of a typo. + +Alternatively, you can force the linter to ignore any line by adding `// eslint-disable-line` after it. + +## Adding Bootstrap + +You don’t have to use [React Bootstrap](https://react-bootstrap.github.io) together with React but it is a popular library for integrating Bootstrap with React apps. If you need it, you can integrate it with Create React App by following these steps: + +Install React Bootstrap and Bootstrap from npm. React Bootstrap does not include Bootstrap CSS so this needs to be installed as well: + +```sh +npm install --save react-bootstrap bootstrap@3 +``` + +Alternatively you may use `yarn`: + +```sh +yarn add react-bootstrap bootstrap@3 +``` + +Import Bootstrap CSS and optionally Bootstrap theme CSS in the beginning of your ```src/index.js``` file: + +```js +import 'bootstrap/dist/css/bootstrap.css'; +import 'bootstrap/dist/css/bootstrap-theme.css'; +// Put any other imports below so that CSS from your +// components takes precedence over default styles. +``` + +Import required React Bootstrap components within ```src/App.js``` file or your custom component files: + +```js +import { Navbar, Jumbotron, Button } from 'react-bootstrap'; +``` + +Now you are ready to use the imported React Bootstrap components within your component hierarchy defined in the render method. Here is an example [`App.js`](https://gist.githubusercontent.com/gaearon/85d8c067f6af1e56277c82d19fd4da7b/raw/6158dd991b67284e9fc8d70b9d973efe87659d72/App.js) redone using React Bootstrap. + +### Using a Custom Theme + +Sometimes you might need to tweak the visual styles of Bootstrap (or equivalent package).<br> +We suggest the following approach: + +* Create a new package that depends on the package you wish to customize, e.g. Bootstrap. +* Add the necessary build steps to tweak the theme, and publish your package on npm. +* Install your own theme npm package as a dependency of your app. + +Here is an example of adding a [customized Bootstrap](https://medium.com/@tacomanator/customizing-create-react-app-aa9ffb88165) that follows these steps. + +## Adding Flow + +Flow is a static type checker that helps you write code with fewer bugs. Check out this [introduction to using static types in JavaScript](https://medium.com/@preethikasireddy/why-use-static-types-in-javascript-part-1-8382da1e0adb) if you are new to this concept. + +Recent versions of [Flow](http://flowtype.org/) work with Create React App projects out of the box. + +To add Flow to a Create React App project, follow these steps: + +1. Run `npm install --save flow-bin` (or `yarn add flow-bin`). +2. Add `"flow": "flow"` to the `scripts` section of your `package.json`. +3. Run `npm run flow init` (or `yarn flow init`) to create a [`.flowconfig` file](https://flowtype.org/docs/advanced-configuration.html) in the root directory. +4. Add `// @flow` to any files you want to type check (for example, to `src/App.js`). + +Now you can run `npm run flow` (or `yarn flow`) to check the files for type errors. +You can optionally use an IDE like [Nuclide](https://nuclide.io/docs/languages/flow/) for a better integrated experience. +In the future we plan to integrate it into Create React App even more closely. + +To learn more about Flow, check out [its documentation](https://flowtype.org/). + +## Adding Custom Environment Variables + +>Note: this feature is available with `react-scripts@0.2.3` and higher. + +Your project can consume variables declared in your environment as if they were declared locally in your JS files. By +default you will have `NODE_ENV` defined for you, and any other environment variables starting with +`REACT_APP_`. + +**The environment variables are embedded during the build time**. Since Create React App produces a static HTML/CSS/JS bundle, it can’t possibly read them at runtime. To read them at runtime, you would need to load HTML into memory on the server and replace placeholders in runtime, just like [described here](#injecting-data-from-the-server-into-the-page). Alternatively you can rebuild the app on the server anytime you change them. + +>Note: You must create custom environment variables beginning with `REACT_APP_`. Any other variables except `NODE_ENV` will be ignored to avoid accidentally [exposing a private key on the machine that could have the same name](https://github.com/facebookincubator/create-react-app/issues/865#issuecomment-252199527). Changing any environment variables will require you to restart the development server if it is running. + +These environment variables will be defined for you on `process.env`. For example, having an environment +variable named `REACT_APP_SECRET_CODE` will be exposed in your JS as `process.env.REACT_APP_SECRET_CODE`. + +There is also a special built-in environment variable called `NODE_ENV`. You can read it from `process.env.NODE_ENV`. When you run `npm start`, it is always equal to `'development'`, when you run `npm test` it is always equal to `'test'`, and when you run `npm run build` to make a production bundle, it is always equal to `'production'`. **You cannot override `NODE_ENV` manually.** This prevents developers from accidentally deploying a slow development build to production. + +These environment variables can be useful for displaying information conditionally based on where the project is +deployed or consuming sensitive data that lives outside of version control. + +First, you need to have environment variables defined. For example, let’s say you wanted to consume a secret defined +in the environment inside a `<form>`: + +```jsx +render() { + return ( + <div> + <small>You are running this application in <b>{process.env.NODE_ENV}</b> mode.</small> + <form> + <input type="hidden" defaultValue={process.env.REACT_APP_SECRET_CODE} /> + </form> + </div> + ); +} +``` + +During the build, `process.env.REACT_APP_SECRET_CODE` will be replaced with the current value of the `REACT_APP_SECRET_CODE` environment variable. Remember that the `NODE_ENV` variable will be set for you automatically. + +When you load the app in the browser and inspect the `<input>`, you will see its value set to `abcdef`, and the bold text will show the environment provided when using `npm start`: + +```html +<div> + <small>You are running this application in <b>development</b> mode.</small> + <form> + <input type="hidden" value="abcdef" /> + </form> +</div> +``` + +The above form is looking for a variable called `REACT_APP_SECRET_CODE` from the environment. In order to consume this +value, we need to have it defined in the environment. This can be done using two ways: either in your shell or in +a `.env` file. Both of these ways are described in the next few sections. + +Having access to the `NODE_ENV` is also useful for performing actions conditionally: + +```js +if (process.env.NODE_ENV !== 'production') { + analytics.disable(); +} +``` + +When you compile the app with `npm run build`, the minification step will strip out this condition, and the resulting bundle will be smaller. + +### Referencing Environment Variables in the HTML + +>Note: this feature is available with `react-scripts@0.9.0` and higher. + +You can also access the environment variables starting with `REACT_APP_` in the `public/index.html`. For example: + +```html +<title>%REACT_APP_WEBSITE_NAME% +``` + +Note that the caveats from the above section apply: + +* Apart from a few built-in variables (`NODE_ENV` and `PUBLIC_URL`), variable names must start with `REACT_APP_` to work. +* The environment variables are injected at build time. If you need to inject them at runtime, [follow this approach instead](#generating-dynamic-meta-tags-on-the-server). + +### Adding Temporary Environment Variables In Your Shell + +Defining environment variables can vary between OSes. It’s also important to know that this manner is temporary for the +life of the shell session. + +#### Windows (cmd.exe) + +```cmd +set REACT_APP_SECRET_CODE=abcdef&&npm start +``` + +(Note: the lack of whitespace is intentional.) + +#### Linux, macOS (Bash) + +```bash +REACT_APP_SECRET_CODE=abcdef npm start +``` + +### Adding Development Environment Variables In `.env` + +>Note: this feature is available with `react-scripts@0.5.0` and higher. + +To define permanent environment variables, create a file called `.env` in the root of your project: + +``` +REACT_APP_SECRET_CODE=abcdef +``` + +`.env` files **should be** checked into source control (with the exclusion of `.env*.local`). + +#### What other `.env` files are can be used? + +>Note: this feature is **available with `react-scripts@1.0.0` and higher**. + +* `.env`: Default. +* `.env.local`: Local overrides. **This file is loaded for all environments except test.** +* `.env.development`, `.env.test`, `.env.production`: Environment-specific settings. +* `.env.development.local`, `.env.test.local`, `.env.production.local`: Local overrides of environment-specific settings. + +Files on the left have more priority than files on the right: + +* `npm start`: `.env.development.local`, `.env.development`, `.env.local`, `.env` +* `npm run build`: `.env.production.local`, `.env.production`, `.env.local`, `.env` +* `npm test`: `.env.test.local`, `.env.test`, `.env` (note `.env.local` is missing) + +These variables will act as the defaults if the machine does not explicitly set them.
+Please refer to the [dotenv documentation](https://github.com/motdotla/dotenv) for more details. + +>Note: If you are defining environment variables for development, your CI and/or hosting platform will most likely need +these defined as well. Consult their documentation how to do this. For example, see the documentation for [Travis CI](https://docs.travis-ci.com/user/environment-variables/) or [Heroku](https://devcenter.heroku.com/articles/config-vars). + +## Can I Use Decorators? + +Many popular libraries use [decorators](https://medium.com/google-developers/exploring-es7-decorators-76ecb65fb841) in their documentation.
+Create React App doesn’t support decorator syntax at the moment because: + +* It is an experimental proposal and is subject to change. +* The current specification version is not officially supported by Babel. +* If the specification changes, we won’t be able to write a codemod because we don’t use them internally at Facebook. + +However in many cases you can rewrite decorator-based code without decorators just as fine.
+Please refer to these two threads for reference: + +* [#214](https://github.com/facebookincubator/create-react-app/issues/214) +* [#411](https://github.com/facebookincubator/create-react-app/issues/411) + +Create React App will add decorator support when the specification advances to a stable stage. + +## Integrating with an API Backend + +These tutorials will help you to integrate your app with an API backend running on another port, +using `fetch()` to access it. + +### Node +Check out [this tutorial](https://www.fullstackreact.com/articles/using-create-react-app-with-a-server/). +You can find the companion GitHub repository [here](https://github.com/fullstackreact/food-lookup-demo). + +### Ruby on Rails + +Check out [this tutorial](https://www.fullstackreact.com/articles/how-to-get-create-react-app-to-work-with-your-rails-api/). +You can find the companion GitHub repository [here](https://github.com/fullstackreact/food-lookup-demo-rails). + +## Proxying API Requests in Development + +>Note: this feature is available with `react-scripts@0.2.3` and higher. + +People often serve the front-end React app from the same host and port as their backend implementation.
+For example, a production setup might look like this after the app is deployed: + +``` +/ - static server returns index.html with React app +/todos - static server returns index.html with React app +/api/todos - server handles any /api/* requests using the backend implementation +``` + +Such setup is **not** required. However, if you **do** have a setup like this, it is convenient to write requests like `fetch('/api/todos')` without worrying about redirecting them to another host or port during development. + +To tell the development server to proxy any unknown requests to your API server in development, add a `proxy` field to your `package.json`, for example: + +```js + "proxy": "http://localhost:4000", +``` + +This way, when you `fetch('/api/todos')` in development, the development server will recognize that it’s not a static asset, and will proxy your request to `http://localhost:4000/api/todos` as a fallback. The development server will only attempt to send requests without a `text/html` accept header to the proxy. + +Conveniently, this avoids [CORS issues](http://stackoverflow.com/questions/21854516/understanding-ajax-cors-and-security-considerations) and error messages like this in development: + +``` +Fetch API cannot load http://localhost:4000/api/todos. No 'Access-Control-Allow-Origin' header is present on the requested resource. Origin 'http://localhost:3000' is therefore not allowed access. If an opaque response serves your needs, set the request's mode to 'no-cors' to fetch the resource with CORS disabled. +``` + +Keep in mind that `proxy` only has effect in development (with `npm start`), and it is up to you to ensure that URLs like `/api/todos` point to the right thing in production. You don’t have to use the `/api` prefix. Any unrecognized request without a `text/html` accept header will be redirected to the specified `proxy`. + +The `proxy` option supports HTTP, HTTPS and WebSocket connections.
+If the `proxy` option is **not** flexible enough for you, alternatively you can: + +* [Configure the proxy yourself](#configuring-the-proxy-manually) +* Enable CORS on your server ([here’s how to do it for Express](http://enable-cors.org/server_expressjs.html)). +* Use [environment variables](#adding-custom-environment-variables) to inject the right server host and port into your app. + +### "Invalid Host Header" Errors After Configuring Proxy + +When you enable the `proxy` option, you opt into a more strict set of host checks. This is necessary because leaving the backend open to remote hosts makes your computer vulnerable to DNS rebinding attacks. The issue is explained in [this article](https://medium.com/webpack/webpack-dev-server-middleware-security-issues-1489d950874a) and [this issue](https://github.com/webpack/webpack-dev-server/issues/887). + +This shouldn’t affect you when developing on `localhost`, but if you develop remotely like [described here](https://github.com/facebookincubator/create-react-app/issues/2271), you will see this error in the browser after enabling the `proxy` option: + +>Invalid Host header + +To work around it, you can specify your public development host in a file called `.env.development` in the root of your project: + +``` +HOST=mypublicdevhost.com +``` + +If you restart the development server now and load the app from the specified host, it should work. + +If you are still having issues or if you’re using a more exotic environment like a cloud editor, you can bypass the host check completely by adding a line to `.env.development.local`. **Note that this is dangerous and exposes your machine to remote code execution from malicious websites:** + +``` +# NOTE: THIS IS DANGEROUS! +# It exposes your machine to attacks from the websites you visit. +DANGEROUSLY_DISABLE_HOST_CHECK=true +``` + +We don’t recommend this approach. + +### Configuring the Proxy Manually + +>Note: this feature is available with `react-scripts@1.0.0` and higher. + +If the `proxy` option is **not** flexible enough for you, you can specify an object in the following form (in `package.json`).
+You may also specify any configuration value [`http-proxy-middleware`](https://github.com/chimurai/http-proxy-middleware#options) or [`http-proxy`](https://github.com/nodejitsu/node-http-proxy#options) supports. +```js +{ + // ... + "proxy": { + "/api": { + "target": "", + "ws": true + // ... + } + } + // ... +} +``` + +All requests matching this path will be proxies, no exceptions. This includes requests for `text/html`, which the standard `proxy` option does not proxy. + +If you need to specify multiple proxies, you may do so by specifying additional entries. +You may also narrow down matches using `*` and/or `**`, to match the path exactly or any subpath. +```js +{ + // ... + "proxy": { + // Matches any request starting with /api + "/api": { + "target": "", + "ws": true + // ... + }, + // Matches any request starting with /foo + "/foo": { + "target": "", + "ssl": true, + "pathRewrite": { + "^/foo": "/foo/beta" + } + // ... + }, + // Matches /bar/abc.html but not /bar/sub/def.html + "/bar/*.html": { + "target": "", + // ... + }, + // Matches /baz/abc.html and /baz/sub/def.html + "/baz/**/*.html": { + "target": "" + // ... + } + } + // ... +} +``` + +### Configuring a WebSocket Proxy + +When setting up a WebSocket proxy, there are a some extra considerations to be aware of. + +If you’re using a WebSocket engine like [Socket.io](https://socket.io/), you must have a Socket.io server running that you can use as the proxy target. Socket.io will not work with a standard WebSocket server. Specifically, don't expect Socket.io to work with [the websocket.org echo test](http://websocket.org/echo.html). + +There’s some good documentation available for [setting up a Socket.io server](https://socket.io/docs/). + +Standard WebSockets **will** work with a standard WebSocket server as well as the websocket.org echo test. You can use libraries like [ws](https://github.com/websockets/ws) for the server, with [native WebSockets in the browser](https://developer.mozilla.org/en-US/docs/Web/API/WebSocket). + +Either way, you can proxy WebSocket requests manually in `package.json`: + +```js +{ + // ... + "proxy": { + "/socket": { + // Your compatible WebSocket server + "target": "ws://", + // Tell http-proxy-middleware that this is a WebSocket proxy. + // Also allows you to proxy WebSocket requests without an additional HTTP request + // https://github.com/chimurai/http-proxy-middleware#external-websocket-upgrade + "ws": true + // ... + } + } + // ... +} +``` + +## Using HTTPS in Development + +>Note: this feature is available with `react-scripts@0.4.0` and higher. + +You may require the dev server to serve pages over HTTPS. One particular case where this could be useful is when using [the "proxy" feature](#proxying-api-requests-in-development) to proxy requests to an API server when that API server is itself serving HTTPS. + +To do this, set the `HTTPS` environment variable to `true`, then start the dev server as usual with `npm start`: + +#### Windows (cmd.exe) + +```cmd +set HTTPS=true&&npm start +``` + +(Note: the lack of whitespace is intentional.) + +#### Linux, macOS (Bash) + +```bash +HTTPS=true npm start +``` + +Note that the server will use a self-signed certificate, so your web browser will almost definitely display a warning upon accessing the page. + +## Generating Dynamic `` Tags on the Server + +Since Create React App doesn’t support server rendering, you might be wondering how to make `` tags dynamic and reflect the current URL. To solve this, we recommend to add placeholders into the HTML, like this: + +```html + + + + + +``` + +Then, on the server, regardless of the backend you use, you can read `index.html` into memory and replace `__OG_TITLE__`, `__OG_DESCRIPTION__`, and any other placeholders with values depending on the current URL. Just make sure to sanitize and escape the interpolated values so that they are safe to embed into HTML! + +If you use a Node server, you can even share the route matching logic between the client and the server. However duplicating it also works fine in simple cases. + +## Pre-Rendering into Static HTML Files + +If you’re hosting your `build` with a static hosting provider you can use [react-snapshot](https://www.npmjs.com/package/react-snapshot) to generate HTML pages for each route, or relative link, in your application. These pages will then seamlessly become active, or “hydrated”, when the JavaScript bundle has loaded. + +There are also opportunities to use this outside of static hosting, to take the pressure off the server when generating and caching routes. + +The primary benefit of pre-rendering is that you get the core content of each page _with_ the HTML payload—regardless of whether or not your JavaScript bundle successfully downloads. It also increases the likelihood that each route of your application will be picked up by search engines. + +You can read more about [zero-configuration pre-rendering (also called snapshotting) here](https://medium.com/superhighfives/an-almost-static-stack-6df0a2791319). + +## Injecting Data from the Server into the Page + +Similarly to the previous section, you can leave some placeholders in the HTML that inject global variables, for example: + +```js + + + + +``` + +Then, on the server, you can replace `__SERVER_DATA__` with a JSON of real data right before sending the response. The client code can then read `window.SERVER_DATA` to use it. **Make sure to [sanitize the JSON before sending it to the client](https://medium.com/node-security/the-most-common-xss-vulnerability-in-react-js-applications-2bdffbcc1fa0) as it makes your app vulnerable to XSS attacks.** + +## Running Tests + +>Note: this feature is available with `react-scripts@0.3.0` and higher.
+>[Read the migration guide to learn how to enable it in older projects!](https://github.com/facebookincubator/create-react-app/blob/master/CHANGELOG.md#migrating-from-023-to-030) + +Create React App uses [Jest](https://facebook.github.io/jest/) as its test runner. To prepare for this integration, we did a [major revamp](https://facebook.github.io/jest/blog/2016/09/01/jest-15.html) of Jest so if you heard bad things about it years ago, give it another try. + +Jest is a Node-based runner. This means that the tests always run in a Node environment and not in a real browser. This lets us enable fast iteration speed and prevent flakiness. + +While Jest provides browser globals such as `window` thanks to [jsdom](https://github.com/tmpvar/jsdom), they are only approximations of the real browser behavior. Jest is intended to be used for unit tests of your logic and your components rather than the DOM quirks. + +We recommend that you use a separate tool for browser end-to-end tests if you need them. They are beyond the scope of Create React App. + +### Filename Conventions + +Jest will look for test files with any of the following popular naming conventions: + +* Files with `.js` suffix in `__tests__` folders. +* Files with `.test.js` suffix. +* Files with `.spec.js` suffix. + +The `.test.js` / `.spec.js` files (or the `__tests__` folders) can be located at any depth under the `src` top level folder. + +We recommend to put the test files (or `__tests__` folders) next to the code they are testing so that relative imports appear shorter. For example, if `App.test.js` and `App.js` are in the same folder, the test just needs to `import App from './App'` instead of a long relative path. Colocation also helps find tests more quickly in larger projects. + +### Command Line Interface + +When you run `npm test`, Jest will launch in the watch mode. Every time you save a file, it will re-run the tests, just like `npm start` recompiles the code. + +The watcher includes an interactive command-line interface with the ability to run all tests, or focus on a search pattern. It is designed this way so that you can keep it open and enjoy fast re-runs. You can learn the commands from the “Watch Usage” note that the watcher prints after every run: + +![Jest watch mode](http://facebook.github.io/jest/img/blog/15-watch.gif) + +### Version Control Integration + +By default, when you run `npm test`, Jest will only run the tests related to files changed since the last commit. This is an optimization designed to make your tests run fast regardless of how many tests you have. However it assumes that you don’t often commit the code that doesn’t pass the tests. + +Jest will always explicitly mention that it only ran tests related to the files changed since the last commit. You can also press `a` in the watch mode to force Jest to run all tests. + +Jest will always run all tests on a [continuous integration](#continuous-integration) server or if the project is not inside a Git or Mercurial repository. + +### Writing Tests + +To create tests, add `it()` (or `test()`) blocks with the name of the test and its code. You may optionally wrap them in `describe()` blocks for logical grouping but this is neither required nor recommended. + +Jest provides a built-in `expect()` global function for making assertions. A basic test could look like this: + +```js +import sum from './sum'; + +it('sums numbers', () => { + expect(sum(1, 2)).toEqual(3); + expect(sum(2, 2)).toEqual(4); +}); +``` + +All `expect()` matchers supported by Jest are [extensively documented here](http://facebook.github.io/jest/docs/expect.html).
+You can also use [`jest.fn()` and `expect(fn).toBeCalled()`](http://facebook.github.io/jest/docs/expect.html#tohavebeencalled) to create “spies” or mock functions. + +### Testing Components + +There is a broad spectrum of component testing techniques. They range from a “smoke test” verifying that a component renders without throwing, to shallow rendering and testing some of the output, to full rendering and testing component lifecycle and state changes. + +Different projects choose different testing tradeoffs based on how often components change, and how much logic they contain. If you haven’t decided on a testing strategy yet, we recommend that you start with creating simple smoke tests for your components: + +```js +import React from 'react'; +import ReactDOM from 'react-dom'; +import App from './App'; + +it('renders without crashing', () => { + const div = document.createElement('div'); + ReactDOM.render(, div); +}); +``` + +This test mounts a component and makes sure that it didn’t throw during rendering. Tests like this provide a lot value with very little effort so they are great as a starting point, and this is the test you will find in `src/App.test.js`. + +When you encounter bugs caused by changing components, you will gain a deeper insight into which parts of them are worth testing in your application. This might be a good time to introduce more specific tests asserting specific expected output or behavior. + +If you’d like to test components in isolation from the child components they render, we recommend using [`shallow()` rendering API](http://airbnb.io/enzyme/docs/api/shallow.html) from [Enzyme](http://airbnb.io/enzyme/). To install it, run: + +```sh +npm install --save enzyme react-test-renderer +``` + +Alternatively you may use `yarn`: + +```sh +yarn add enzyme react-test-renderer +``` + +You can write a smoke test with it too: + +```js +import React from 'react'; +import { shallow } from 'enzyme'; +import App from './App'; + +it('renders without crashing', () => { + shallow(); +}); +``` + +Unlike the previous smoke test using `ReactDOM.render()`, this test only renders `` and doesn’t go deeper. For example, even if `` itself renders a ` + + + ); + } else if ((this.state.context === undefined) || + (this.state.loadState === LoadState.Working)) { return (
loading
); } + let context: EteSyncContextType = this.state.context; + return (
@@ -52,11 +120,11 @@ export class EteSyncContext extends React.Component { } + render={() => } /> } + render={({match}) => } />
From 295867a4399457a8f12d0246e577723b47117ac1 Mon Sep 17 00:00:00 2001 From: Tom Hacohen Date: Mon, 4 Dec 2017 10:03:14 +0000 Subject: [PATCH 024/912] Login form: add a missing prevent default. --- src/EteSyncContext.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/EteSyncContext.tsx b/src/EteSyncContext.tsx index 1309ab5..1ba9ae7 100644 --- a/src/EteSyncContext.tsx +++ b/src/EteSyncContext.tsx @@ -52,7 +52,8 @@ export class EteSyncContext extends React.Component { } } - generateEncryption() { + generateEncryption(e: any) { + e.preventDefault(); let authenticator = new EteSync.Authenticator(SERVICE_API); this.setState({ From d7c90ccb0ab6a2e9f6befd76975edc559a095306 Mon Sep 17 00:00:00 2001 From: Tom Hacohen Date: Mon, 4 Dec 2017 13:28:42 +0000 Subject: [PATCH 025/912] Move to material-ui. --- package.json | 2 + src/App.css | 11 ++- src/App.tsx | 91 ++++++++++++++++++++++- src/Constants.tsx | 7 ++ src/EteSyncContext.tsx | 6 +- src/images/logo.svg | 162 +++++++++++++++++++++++++++++++++++++++++ src/logo.svg | 7 -- yarn.lock | 118 +++++++++++++++++++++++++++++- 8 files changed, 384 insertions(+), 20 deletions(-) create mode 100644 src/Constants.tsx create mode 100644 src/images/logo.svg delete mode 100644 src/logo.svg diff --git a/package.json b/package.json index ddf69a2..b019e03 100644 --- a/package.json +++ b/package.json @@ -5,6 +5,7 @@ "dependencies": { "ical.js": "^1.2.2", "isomorphic-fetch": "^2.1.1", + "material-ui": "^0.20.0", "react": "^16.2.0", "react-dom": "^16.2.0", "react-router-dom": "^4.2.2", @@ -21,6 +22,7 @@ "devDependencies": { "@types/isomorphic-fetch": "^0.0.34", "@types/jest": "^21.1.8", + "@types/material-ui": "^0.18.5", "@types/node": "^8.0.53", "@types/react": "^16.0.25", "@types/react-dom": "^16.0.3", diff --git a/src/App.css b/src/App.css index 15adfdc..dbba618 100644 --- a/src/App.css +++ b/src/App.css @@ -18,7 +18,12 @@ font-size: large; } -@keyframes App-logo-spin { - from { transform: rotate(0deg); } - to { transform: rotate(360deg); } +.App-drawer-header { + background-color: #555; + padding: 10px; +} + +.App-drawer-logo { + width: 60px; + margin-bottom: 10px; } diff --git a/src/App.tsx b/src/App.tsx index 872b96d..546fa23 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,10 +1,47 @@ import * as React from 'react'; import { HashRouter } from 'react-router-dom'; +import getMuiTheme from 'material-ui/styles/getMuiTheme'; +import MuiThemeProvider from 'material-ui/styles/MuiThemeProvider'; +import { amber500, amber700, lightBlue500, darkBlack, white } from 'material-ui/styles/colors'; +import AppBar from 'material-ui/AppBar'; +import Drawer from 'material-ui/Drawer'; +import IconButton from 'material-ui/IconButton'; +import { List, ListItem } from 'material-ui/List'; +import ActionCode from 'material-ui/svg-icons/action/code'; +import ActionHome from 'material-ui/svg-icons/action/home'; +import ActionBugReport from 'material-ui/svg-icons/action/bug-report'; +import ActionQuestionAnswer from 'material-ui/svg-icons/action/question-answer'; + +import NavigationMenu from 'material-ui/svg-icons/navigation/menu'; + import './App.css'; import { EteSyncContext } from './EteSyncContext'; import { RouteResolver } from './routes'; +import * as C from './Constants'; + +const logo = require('./images/logo.svg'); + +const muiTheme = getMuiTheme({ + palette: { + primary1Color: amber500, + primary2Color: amber700, + accent1Color: lightBlue500, + textColor: darkBlack, + alternateTextColor: white, + } +}); + +function getPalette(part: string): string { + const theme = muiTheme; + if ((theme.palette === undefined) || (theme.palette[part] === undefined)) { + return ''; + } + + return theme.palette[part]; +} + export const routeResolver = new RouteResolver({ home: '', journals: { @@ -28,11 +65,59 @@ export const routeResolver = new RouteResolver({ }); class App extends React.Component { + state: { + drawerOpen: boolean, + }; + + constructor(props: any) { + super(props); + this.state = { drawerOpen: false }; + + this.toggleDrawer = this.toggleDrawer.bind(this); + this.closeDrawer = this.closeDrawer.bind(this); + } + + toggleDrawer() { + this.setState({drawerOpen: !this.state.drawerOpen}); + } + + closeDrawer() { + this.setState({drawerOpen: false}); + } + render() { return ( - - - + +
+ } + /> + +
+ +
+ {C.appName} +
+
+ + } href={C.homePage} /> + } href={C.faq} /> + } href={C.sourceCode} /> + } href={C.reportIssue} /> + +
+ + + + +
+
); } } diff --git a/src/Constants.tsx b/src/Constants.tsx new file mode 100644 index 0000000..301fe8d --- /dev/null +++ b/src/Constants.tsx @@ -0,0 +1,7 @@ +export const appName = 'EteSync'; +export const homePage = 'https://www.etesync.com/'; +export const faq = homePage + 'faq/'; +export const sourceCode = 'https://github.com/etesync/etesync-web'; +export const reportIssue = sourceCode + '/issues'; + +export const serviceApiBase = 'http://localhost:8000'; diff --git a/src/EteSyncContext.tsx b/src/EteSyncContext.tsx index 1ba9ae7..fa25a3e 100644 --- a/src/EteSyncContext.tsx +++ b/src/EteSyncContext.tsx @@ -8,7 +8,7 @@ import * as EteSync from './api/EteSync'; import { routeResolver } from './App'; -const SERVICE_API = 'http://localhost:8000'; +import * as C from './Constants'; const CONTEXT_SESSION_KEY = 'EteSyncContext'; @@ -54,7 +54,7 @@ export class EteSyncContext extends React.Component { generateEncryption(e: any) { e.preventDefault(); - let authenticator = new EteSync.Authenticator(SERVICE_API); + let authenticator = new EteSync.Authenticator(C.serviceApiBase); this.setState({ loadState: LoadState.Working @@ -69,7 +69,7 @@ export class EteSyncContext extends React.Component { const derived = EteSync.deriveKey(username, encryptionPassword); const context = { - serviceApiUrl: SERVICE_API, + serviceApiUrl: C.serviceApiBase, credentials, encryptionKey: derived, }; diff --git a/src/images/logo.svg b/src/images/logo.svg new file mode 100644 index 0000000..009f1a8 --- /dev/null +++ b/src/images/logo.svg @@ -0,0 +1,162 @@ + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/logo.svg b/src/logo.svg deleted file mode 100644 index 6b60c10..0000000 --- a/src/logo.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - - diff --git a/yarn.lock b/yarn.lock index 2414edf..315756b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -18,10 +18,23 @@ version "3.2.16" resolved "https://registry.yarnpkg.com/@types/jquery/-/jquery-3.2.16.tgz#04419c404a3194350e7d3f339a90e72c88db3111" +"@types/material-ui@^0.18.5": + version "0.18.5" + resolved "https://registry.yarnpkg.com/@types/material-ui/-/material-ui-0.18.5.tgz#27f791226cc297f5df58ff811067fa44c5599dd3" + dependencies: + "@types/react" "*" + "@types/react-addons-linked-state-mixin" "*" + "@types/node@*", "@types/node@^8.0.53": version "8.0.53" resolved "https://registry.yarnpkg.com/@types/node/-/node-8.0.53.tgz#396b35af826fa66aad472c8cb7b8d5e277f4e6d8" +"@types/react-addons-linked-state-mixin@*": + version "0.14.18" + resolved "https://registry.yarnpkg.com/@types/react-addons-linked-state-mixin/-/react-addons-linked-state-mixin-0.14.18.tgz#6977ae59e19aa0ce3a5d7a9e057961aaf1afe959" + dependencies: + "@types/react" "*" + "@types/react-dom@^16.0.3": version "16.0.3" resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-16.0.3.tgz#8accad7eabdab4cca3e1a56f5ccb57de2da0ff64" @@ -449,7 +462,7 @@ babel-register@^6.26.0: mkdirp "^0.5.1" source-map-support "^0.4.15" -babel-runtime@^6.22.0, babel-runtime@^6.26.0: +babel-runtime@^6.22.0, babel-runtime@^6.23.0, babel-runtime@^6.26.0: version "6.26.0" resolved "https://registry.yarnpkg.com/babel-runtime/-/babel-runtime-6.26.0.tgz#965c7058668e82b55d7bfe04ff2337bc8b5647fe" dependencies: @@ -585,6 +598,10 @@ boom@5.x.x: dependencies: hoek "4.x.x" +bowser@^1.7.3: + version "1.8.1" + resolved "https://registry.yarnpkg.com/bowser/-/bowser-1.8.1.tgz#49785777e7302febadb1a5b71d9a646520ed310d" + boxen@^0.6.0: version "0.6.0" resolved "https://registry.yarnpkg.com/boxen/-/boxen-0.6.0.tgz#8364d4248ac34ff0ef1b2f2bf49a6c60ce0d81b6" @@ -800,6 +817,10 @@ center-align@^0.1.1: align-text "^0.1.3" lazy-cache "^1.0.3" +chain-function@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/chain-function/-/chain-function-1.0.0.tgz#0d4ab37e7e18ead0bdc47b920764118ce58733dc" + chalk@1.1.3, chalk@^1.0.0, chalk@^1.1.1, chalk@^1.1.3: version "1.1.3" resolved "https://registry.yarnpkg.com/chalk/-/chalk-1.1.3.tgz#a8115c55e4a702fe4d150abd3872822a7e09fc98" @@ -818,6 +839,10 @@ chalk@^2.0.0, chalk@^2.0.1, chalk@^2.1.0, chalk@^2.3.0: escape-string-regexp "^1.0.5" supports-color "^4.0.0" +change-emitter@^0.1.2: + version "0.1.6" + resolved "https://registry.yarnpkg.com/change-emitter/-/change-emitter-0.1.6.tgz#e8b2fe3d7f1ab7d69a32199aff91ea6931409515" + chardet@^0.4.0: version "0.4.2" resolved "https://registry.yarnpkg.com/chardet/-/chardet-0.4.2.tgz#b5473b33dc97c424e5d98dc87d55d4d8a29c8bf2" @@ -1129,6 +1154,12 @@ css-color-names@0.0.4: version "0.0.4" resolved "https://registry.yarnpkg.com/css-color-names/-/css-color-names-0.0.4.tgz#808adc2e79cf84738069b646cb20ec27beb629e0" +css-in-js-utils@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/css-in-js-utils/-/css-in-js-utils-2.0.0.tgz#5af1dd70f4b06b331f48d22a3d86e0786c0b9435" + dependencies: + hyphenate-style-name "^1.0.2" + css-loader@0.28.4: version "0.28.4" resolved "https://registry.yarnpkg.com/css-loader/-/css-loader-0.28.4.tgz#6cf3579192ce355e8b38d5f42dd7a1f2ec898d0f" @@ -1389,6 +1420,10 @@ dom-converter@~0.1: dependencies: utila "~0.3" +dom-helpers@^3.2.0: + version "3.2.1" + resolved "https://registry.yarnpkg.com/dom-helpers/-/dom-helpers-3.2.1.tgz#3203e07fed217bd1f424b019735582fc37b2825a" + dom-serializer@0: version "0.1.0" resolved "https://registry.yarnpkg.com/dom-serializer/-/dom-serializer-0.1.0.tgz#073c697546ce0780ce23be4a28e293e40bc30c82" @@ -1803,7 +1838,7 @@ fb-watchman@^2.0.0: dependencies: bser "^2.0.0" -fbjs@^0.8.16: +fbjs@^0.8.1, fbjs@^0.8.16: version "0.8.16" resolved "https://registry.yarnpkg.com/fbjs/-/fbjs-0.8.16.tgz#5e67432f550dc41b572bf55847b8aca64e5337db" dependencies: @@ -2248,7 +2283,7 @@ hoek@4.x.x: version "4.2.0" resolved "https://registry.yarnpkg.com/hoek/-/hoek-4.2.0.tgz#72d9d0754f7fe25ca2d01ad8f8f9a9449a89526d" -hoist-non-react-statics@^2.3.0: +hoist-non-react-statics@^2.3.0, hoist-non-react-statics@^2.3.1: version "2.3.1" resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-2.3.1.tgz#343db84c6018c650778898240135a1420ee22ce0" @@ -2378,6 +2413,10 @@ https-browserify@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/https-browserify/-/https-browserify-1.0.0.tgz#ec06c10e0a34c0f2faf199f7fd7fc78fffd03c73" +hyphenate-style-name@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/hyphenate-style-name/-/hyphenate-style-name-1.0.2.tgz#31160a36930adaf1fc04c6074f7eb41465d4ec4b" + ical.js@^1.2.2: version "1.2.2" resolved "https://registry.yarnpkg.com/ical.js/-/ical.js-1.2.2.tgz#59b517362a8f61dce0342fe67deb7c20dd119f6e" @@ -2437,6 +2476,13 @@ ini@^1.3.4, ini@~1.3.0: version "1.3.5" resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.5.tgz#eee25f56db1c9ec6085e0c22778083f596abf927" +inline-style-prefixer@^3.0.8: + version "3.0.8" + resolved "https://registry.yarnpkg.com/inline-style-prefixer/-/inline-style-prefixer-3.0.8.tgz#8551b8e5b4d573244e66a34b04f7d32076a2b534" + dependencies: + bowser "^1.7.3" + css-in-js-utils "^2.0.0" + inquirer@3.3.0: version "3.3.0" resolved "https://registry.yarnpkg.com/inquirer/-/inquirer-3.3.0.tgz#9dd2f2ad765dcab1ff0443b491442a20ba227dc9" @@ -3080,6 +3126,10 @@ jsprim@^1.2.2: json-schema "0.2.3" verror "1.10.0" +keycode@^2.1.8: + version "2.1.9" + resolved "https://registry.yarnpkg.com/keycode/-/keycode-2.1.9.tgz#964a23c54e4889405b4861a5c9f0480d45141dfa" + kind-of@^3.0.2: version "3.2.2" resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-3.2.2.tgz#31ea21a734bab9bbb0f32466d893aea51e4a3c64" @@ -3192,6 +3242,10 @@ lodash.memoize@^4.1.2: version "4.1.2" resolved "https://registry.yarnpkg.com/lodash.memoize/-/lodash.memoize-4.1.2.tgz#bcc6c49a42a2840ed997f323eada5ecd182e0bfe" +lodash.merge@^4.6.0: + version "4.6.0" + resolved "https://registry.yarnpkg.com/lodash.merge/-/lodash.merge-4.6.0.tgz#69884ba144ac33fe699737a6086deffadd0f89c5" + lodash.template@^4.4.0: version "4.4.0" resolved "https://registry.yarnpkg.com/lodash.template/-/lodash.template-4.4.0.tgz#e73a0385c8355591746e020b99679c690e68fba0" @@ -3205,6 +3259,10 @@ lodash.templatesettings@^4.0.0: dependencies: lodash._reinterpolate "~3.0.0" +lodash.throttle@^4.1.1: + version "4.1.1" + resolved "https://registry.yarnpkg.com/lodash.throttle/-/lodash.throttle-4.1.1.tgz#c23e91b710242ac70c37f1e1cda9274cc39bf2f4" + lodash.uniq@^4.5.0: version "4.5.0" resolved "https://registry.yarnpkg.com/lodash.uniq/-/lodash.uniq-4.5.0.tgz#d0225373aeb652adc1bc82e4945339a842754773" @@ -3263,6 +3321,22 @@ map-obj@^1.0.0, map-obj@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/map-obj/-/map-obj-1.0.1.tgz#d933ceb9205d82bdcf4886f6742bdc2b4dea146d" +material-ui@^0.20.0: + version "0.20.0" + resolved "https://registry.yarnpkg.com/material-ui/-/material-ui-0.20.0.tgz#85411bb59c916c9c7703f29dcffc44e3a67d5111" + dependencies: + babel-runtime "^6.23.0" + inline-style-prefixer "^3.0.8" + keycode "^2.1.8" + lodash.merge "^4.6.0" + lodash.throttle "^4.1.1" + prop-types "^15.5.7" + react-event-listener "^0.5.1" + react-transition-group "^1.2.1" + recompose "^0.26.0" + simple-assign "^0.1.0" + warning "^3.0.0" + math-expression-evaluator@^1.2.14: version "1.2.17" resolved "https://registry.yarnpkg.com/math-expression-evaluator/-/math-expression-evaluator-1.2.17.tgz#de819fdbcd84dccd8fae59c6aeb79615b9d266ac" @@ -4221,7 +4295,7 @@ promise@^7.1.1: dependencies: asap "~2.0.3" -prop-types@^15.5.4, prop-types@^15.6.0: +prop-types@^15.5.4, prop-types@^15.5.6, prop-types@^15.5.7, prop-types@^15.6.0: version "15.6.0" resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.6.0.tgz#ceaf083022fc46b4a35f69e13ef75aed0d639856" dependencies: @@ -4375,6 +4449,15 @@ react-error-overlay@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/react-error-overlay/-/react-error-overlay-3.0.0.tgz#c2bc8f4d91f1375b3dad6d75265d51cd5eeaf655" +react-event-listener@^0.5.1: + version "0.5.1" + resolved "https://registry.yarnpkg.com/react-event-listener/-/react-event-listener-0.5.1.tgz#ba36076e47bc37c5a67ff5ccd4a9ff0f15621040" + dependencies: + babel-runtime "^6.26.0" + fbjs "^0.8.16" + prop-types "^15.6.0" + warning "^3.0.0" + react-router-dom@^4.2.2: version "4.2.2" resolved "https://registry.yarnpkg.com/react-router-dom/-/react-router-dom-4.2.2.tgz#c8a81df3adc58bba8a76782e946cbd4eae649b8d" @@ -4434,6 +4517,16 @@ react-scripts-ts@2.8.0: optionalDependencies: fsevents "1.1.2" +react-transition-group@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/react-transition-group/-/react-transition-group-1.2.1.tgz#e11f72b257f921b213229a774df46612346c7ca6" + dependencies: + chain-function "^1.0.0" + dom-helpers "^3.2.0" + loose-envify "^1.3.1" + prop-types "^15.5.6" + warning "^3.0.0" + react@^16.2.0: version "16.2.0" resolved "https://registry.yarnpkg.com/react/-/react-16.2.0.tgz#a31bd2dab89bff65d42134fa187f24d054c273ba" @@ -4510,6 +4603,15 @@ readdirp@^2.0.0: readable-stream "^2.0.2" set-immediate-shim "^1.0.1" +recompose@^0.26.0: + version "0.26.0" + resolved "https://registry.yarnpkg.com/recompose/-/recompose-0.26.0.tgz#9babff039cb72ba5bd17366d55d7232fbdfb2d30" + dependencies: + change-emitter "^0.1.2" + fbjs "^0.8.1" + hoist-non-react-statics "^2.3.1" + symbol-observable "^1.0.4" + recursive-readdir@2.2.1: version "2.2.1" resolved "https://registry.yarnpkg.com/recursive-readdir/-/recursive-readdir-2.2.1.tgz#90ef231d0778c5ce093c9a48d74e5c5422d13a99" @@ -4890,6 +4992,10 @@ signal-exit@^3.0.0, signal-exit@^3.0.2: version "3.0.2" resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.2.tgz#b5fdc08f1287ea1178628e415e25132b73646c6d" +simple-assign@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/simple-assign/-/simple-assign-0.1.0.tgz#17fd3066a5f3d7738f50321bb0f14ca281cc4baa" + "sjcl@git+https://github.com/etesync/sjcl": version "1.0.7" resolved "git+https://github.com/etesync/sjcl#5cdf1d4d1d9e8e19fffdf6b498b5c7c073995abb" @@ -5194,6 +5300,10 @@ sw-toolbox@^3.4.0: path-to-regexp "^1.0.1" serviceworker-cache-polyfill "^4.0.0" +symbol-observable@^1.0.4: + version "1.1.0" + resolved "https://registry.yarnpkg.com/symbol-observable/-/symbol-observable-1.1.0.tgz#5c68fd8d54115d9dfb72a84720549222e8db9b32" + symbol-tree@^3.2.1: version "3.2.2" resolved "https://registry.yarnpkg.com/symbol-tree/-/symbol-tree-3.2.2.tgz#ae27db38f660a7ae2e1c3b7d1bc290819b8519e6" From 0a05095da2c1bb2e09e5f9f06a19c8300b67890b Mon Sep 17 00:00:00 2001 From: Tom Hacohen Date: Mon, 4 Dec 2017 14:13:41 +0000 Subject: [PATCH 026/912] Home: redirect to the journals view. --- src/EteSyncContext.tsx | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/EteSyncContext.tsx b/src/EteSyncContext.tsx index fa25a3e..70d3956 100644 --- a/src/EteSyncContext.tsx +++ b/src/EteSyncContext.tsx @@ -1,5 +1,5 @@ import * as React from 'react'; -import { Switch, Route } from 'react-router'; +import { Switch, Route, Redirect } from 'react-router'; import { JournalList } from './JournalList'; import { JournalView } from './JournalView'; @@ -121,6 +121,11 @@ export class EteSyncContext extends React.Component { } + /> + } /> Date: Mon, 4 Dec 2017 14:44:52 +0000 Subject: [PATCH 027/912] Change to the production API and allow setting a custom server. --- src/Constants.tsx | 2 +- src/EteSyncContext.tsx | 19 ++++++++++++++++--- 2 files changed, 17 insertions(+), 4 deletions(-) diff --git a/src/Constants.tsx b/src/Constants.tsx index 301fe8d..d2d2f49 100644 --- a/src/Constants.tsx +++ b/src/Constants.tsx @@ -4,4 +4,4 @@ export const faq = homePage + 'faq/'; export const sourceCode = 'https://github.com/etesync/etesync-web'; export const reportIssue = sourceCode + '/issues'; -export const serviceApiBase = 'http://localhost:8000'; +export const serviceApiBase = 'https://api.etesync.com/'; diff --git a/src/EteSyncContext.tsx b/src/EteSyncContext.tsx index 70d3956..fd014b1 100644 --- a/src/EteSyncContext.tsx +++ b/src/EteSyncContext.tsx @@ -25,6 +25,7 @@ export interface EteSyncContextType { } export class EteSyncContext extends React.Component { + server: HTMLInputElement; username: HTMLInputElement; password: HTMLInputElement; encryptionPassword: HTMLInputElement; @@ -54,7 +55,9 @@ export class EteSyncContext extends React.Component { generateEncryption(e: any) { e.preventDefault(); - let authenticator = new EteSync.Authenticator(C.serviceApiBase); + const server = this.server.value; + + let authenticator = new EteSync.Authenticator(server); this.setState({ loadState: LoadState.Working @@ -69,7 +72,7 @@ export class EteSyncContext extends React.Component { const derived = EteSync.deriveKey(username, encryptionPassword); const context = { - serviceApiUrl: C.serviceApiBase, + serviceApiUrl: server, credentials, encryptionKey: derived, }; @@ -95,12 +98,22 @@ export class EteSyncContext extends React.Component { {(this.state.error !== undefined) && (
Error! {this.state.error.message}
)}
this.username = input as HTMLInputElement} /> - this.password = input as HTMLInputElement} /> + this.password = input as HTMLInputElement} + /> this.encryptionPassword = input as HTMLInputElement} /> + this.server = input as HTMLInputElement} + defaultValue={C.serviceApiBase} + />
From 622805a5b6d11da91253dc5fb70eebcc5a10cd1e Mon Sep 17 00:00:00 2001 From: Tom Hacohen Date: Mon, 4 Dec 2017 15:05:35 +0000 Subject: [PATCH 028/912] Improve the look and feel of the login form. --- src/EteSyncContext.tsx | 74 ++++++++++++++++++++++++++++++------------ 1 file changed, 53 insertions(+), 21 deletions(-) diff --git a/src/EteSyncContext.tsx b/src/EteSyncContext.tsx index fd014b1..c9e5355 100644 --- a/src/EteSyncContext.tsx +++ b/src/EteSyncContext.tsx @@ -1,5 +1,8 @@ import * as React from 'react'; import { Switch, Route, Redirect } from 'react-router'; +import RaisedButton from 'material-ui/RaisedButton'; +import TextField from 'material-ui/TextField'; +import Toggle from 'material-ui/Toggle'; import { JournalList } from './JournalList'; import { JournalView } from './JournalView'; @@ -25,14 +28,15 @@ export interface EteSyncContextType { } export class EteSyncContext extends React.Component { - server: HTMLInputElement; - username: HTMLInputElement; - password: HTMLInputElement; - encryptionPassword: HTMLInputElement; + server: TextField; + username: TextField; + password: TextField; + encryptionPassword: TextField; state: { context?: EteSyncContextType; loadState: LoadState; + showAdvanced?: boolean; error?: Error; }; @@ -40,6 +44,7 @@ export class EteSyncContext extends React.Component { super(props); this.state = {loadState: LoadState.Initial}; this.generateEncryption = this.generateEncryption.bind(this); + this.toggleAdvancedSettings = this.toggleAdvancedSettings.bind(this); const contextStr = sessionStorage.getItem(CONTEXT_SESSION_KEY); @@ -55,7 +60,7 @@ export class EteSyncContext extends React.Component { generateEncryption(e: any) { e.preventDefault(); - const server = this.server.value; + const server = this.state.showAdvanced ? this.server.getValue() : C.serviceApiBase; let authenticator = new EteSync.Authenticator(server); @@ -63,9 +68,9 @@ export class EteSyncContext extends React.Component { loadState: LoadState.Working }); - const username = this.username.value; - const password = this.password.value; - const encryptionPassword = this.encryptionPassword.value; + const username = this.username.getValue(); + const password = this.password.getValue(); + const encryptionPassword = this.encryptionPassword.getValue(); authenticator.getAuthToken(username, password).then((authToken) => { const credentials = new EteSync.Credentials(username, authToken); @@ -91,30 +96,57 @@ export class EteSyncContext extends React.Component { }); } + toggleAdvancedSettings() { + this.setState(Object.assign( + {}, this.state, + {showAdvanced: !this.state.showAdvanced})); + } + render() { if (this.state.loadState === LoadState.Initial) { + let advancedSettings = null; + if (this.state.showAdvanced) { + advancedSettings = ( +
+ this.server = input as TextField} + /> +
+
+ ); + } + return (
{(this.state.error !== undefined) && (
Error! {this.state.error.message}
)}
- this.username = input as HTMLInputElement} /> - this.username = input as TextField} + /> +
+ this.password = input as HTMLInputElement} + floatingLabelText="Password" + ref={(input) => this.password = input as TextField} /> - + this.encryptionPassword = input as HTMLInputElement} + floatingLabelText="Encryption Password" + ref={(input) => this.encryptionPassword = input as TextField} /> - this.server = input as HTMLInputElement} - defaultValue={C.serviceApiBase} +
+ - + {advancedSettings} +
); From 516bdd26223e052b1c9b6d87fb7d8678d6d71eb5 Mon Sep 17 00:00:00 2001 From: Tom Hacohen Date: Mon, 4 Dec 2017 15:41:18 +0000 Subject: [PATCH 029/912] Improve log in look and feel. --- src/App.tsx | 2 +- src/Constants.tsx | 2 + src/EteSyncContext.tsx | 91 ++++++++++++++++++++++++++++-------------- 3 files changed, 64 insertions(+), 31 deletions(-) diff --git a/src/App.tsx b/src/App.tsx index 546fa23..747a5a6 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -33,7 +33,7 @@ const muiTheme = getMuiTheme({ } }); -function getPalette(part: string): string { +export function getPalette(part: string): string { const theme = muiTheme; if ((theme.palette === undefined) || (theme.palette[part] === undefined)) { return ''; diff --git a/src/Constants.tsx b/src/Constants.tsx index d2d2f49..a55aeb0 100644 --- a/src/Constants.tsx +++ b/src/Constants.tsx @@ -4,4 +4,6 @@ export const faq = homePage + 'faq/'; export const sourceCode = 'https://github.com/etesync/etesync-web'; export const reportIssue = sourceCode + '/issues'; +export const forgotPassword = 'https://www.etesync.com/accounts/password/reset/'; + export const serviceApiBase = 'https://api.etesync.com/'; diff --git a/src/EteSyncContext.tsx b/src/EteSyncContext.tsx index c9e5355..deeaca2 100644 --- a/src/EteSyncContext.tsx +++ b/src/EteSyncContext.tsx @@ -1,5 +1,6 @@ import * as React from 'react'; import { Switch, Route, Redirect } from 'react-router'; +import Paper from 'material-ui/Paper'; import RaisedButton from 'material-ui/RaisedButton'; import TextField from 'material-ui/TextField'; import Toggle from 'material-ui/Toggle'; @@ -9,7 +10,7 @@ import { JournalView } from './JournalView'; import * as EteSync from './api/EteSync'; -import { routeResolver } from './App'; +import { routeResolver, getPalette } from './App'; import * as C from './Constants'; @@ -118,36 +119,66 @@ export class EteSyncContext extends React.Component { ); } + const styles = { + holder: { + margin: 'auto', + maxWidth: 400, + padding: 20, + }, + paper: { + padding: 20, + }, + form: { + }, + forgotPassword: { + color: getPalette('accent1Color'), + paddingTop: 20, + }, + advancedSettings: { + marginTop: 20, + }, + submit: { + marginTop: 40, + textAlign: 'right', + }, + }; + return ( -
- {(this.state.error !== undefined) && (
Error! {this.state.error.message}
)} -
- this.username = input as TextField} - /> -
- this.password = input as TextField} - /> -
- this.encryptionPassword = input as TextField} - /> -
- - {advancedSettings} - - +
); } else if ((this.state.context === undefined) || From f8ee484c3bb68dedd25da6669e741e20ba252eae Mon Sep 17 00:00:00 2001 From: Tom Hacohen Date: Mon, 4 Dec 2017 15:58:24 +0000 Subject: [PATCH 030/912] Add some login form validation. --- src/EteSyncContext.tsx | 47 +++++++++++++++++++++++++++++++++++------- 1 file changed, 40 insertions(+), 7 deletions(-) diff --git a/src/EteSyncContext.tsx b/src/EteSyncContext.tsx index deeaca2..4a66849 100644 --- a/src/EteSyncContext.tsx +++ b/src/EteSyncContext.tsx @@ -28,6 +28,13 @@ export interface EteSyncContextType { encryptionKey: string; } +interface FormErrors { + errorEmail?: string; + errorPassword?: string; + errorEncryptionPassword?: string; + errorServer?: string; +} + export class EteSyncContext extends React.Component { server: TextField; username: TextField; @@ -39,11 +46,15 @@ export class EteSyncContext extends React.Component { loadState: LoadState; showAdvanced?: boolean; error?: Error; + errors: FormErrors; }; constructor(props: any) { super(props); - this.state = {loadState: LoadState.Initial}; + this.state = { + loadState: LoadState.Initial, + errors: {}, + }; this.generateEncryption = this.generateEncryption.bind(this); this.toggleAdvancedSettings = this.toggleAdvancedSettings.bind(this); @@ -52,10 +63,10 @@ export class EteSyncContext extends React.Component { if (contextStr !== null) { const context: EteSyncContextType = JSON.parse(contextStr); - this.state = { + this.state = Object.assign({}, this.state, { loadState: LoadState.Done, context - }; + }); } } @@ -65,14 +76,32 @@ export class EteSyncContext extends React.Component { let authenticator = new EteSync.Authenticator(server); - this.setState({ - loadState: LoadState.Working - }); - const username = this.username.getValue(); const password = this.password.getValue(); const encryptionPassword = this.encryptionPassword.getValue(); + let errors: FormErrors = {}; + const fieldRequired = 'This field is required!'; + if (!username) { + errors.errorEmail = fieldRequired; + } + if (!password) { + errors.errorPassword = fieldRequired; + } + if (!encryptionPassword) { + errors.errorEncryptionPassword = fieldRequired; + } + if (Object.keys(errors).length) { + this.setState(Object.assign( + {}, this.state, + {errors: errors})); + return; + } + + this.setState({ + loadState: LoadState.Working + }); + authenticator.getAuthToken(username, password).then((authToken) => { const credentials = new EteSync.Credentials(username, authToken); const derived = EteSync.deriveKey(username, encryptionPassword); @@ -111,6 +140,7 @@ export class EteSyncContext extends React.Component {
this.server = input as TextField} /> @@ -151,11 +181,13 @@ export class EteSyncContext extends React.Component {
this.username = input as TextField} /> this.password = input as TextField} /> @@ -164,6 +196,7 @@ export class EteSyncContext extends React.Component {
this.encryptionPassword = input as TextField} /> From 3faa7b3ed6eb74b61fb2810af4859d5a43bf9eac Mon Sep 17 00:00:00 2001 From: Tom Hacohen Date: Mon, 4 Dec 2017 16:50:54 +0000 Subject: [PATCH 031/912] Simplify calls to setState, apparently it already shallow merges. --- src/EteSyncContext.tsx | 8 ++------ src/JournalView.tsx | 4 ++-- 2 files changed, 4 insertions(+), 8 deletions(-) diff --git a/src/EteSyncContext.tsx b/src/EteSyncContext.tsx index 4a66849..f1da1e3 100644 --- a/src/EteSyncContext.tsx +++ b/src/EteSyncContext.tsx @@ -92,9 +92,7 @@ export class EteSyncContext extends React.Component { errors.errorEncryptionPassword = fieldRequired; } if (Object.keys(errors).length) { - this.setState(Object.assign( - {}, this.state, - {errors: errors})); + this.setState({errors: errors}); return; } @@ -127,9 +125,7 @@ export class EteSyncContext extends React.Component { } toggleAdvancedSettings() { - this.setState(Object.assign( - {}, this.state, - {showAdvanced: !this.state.showAdvanced})); + this.setState({showAdvanced: !this.state.showAdvanced}); } render() { diff --git a/src/JournalView.tsx b/src/JournalView.tsx index 422045b..c63d42d 100644 --- a/src/JournalView.tsx +++ b/src/JournalView.tsx @@ -39,12 +39,12 @@ export class JournalView extends React.Component { let journalManager = new EteSync.JournalManager(credentials, apiBase); journalManager.fetch(journal).then((journalInstance) => { - this.setState(Object.assign({}, this.state, { journal: journalInstance })); + this.setState({ journal: journalInstance }); }); let entryManager = new EteSync.EntryManager(credentials, apiBase, journal); entryManager.list(this.props.prevUid || null).then((entries) => { - this.setState(Object.assign({}, this.state, { entries })); + this.setState({ entries }); }); } From b668e6478a60aef7862ec579935b997ca6894aab Mon Sep 17 00:00:00 2001 From: Tom Hacohen Date: Mon, 4 Dec 2017 17:13:35 +0000 Subject: [PATCH 032/912] Organise calendars and address books in lists. --- src/JournalList.tsx | 38 ++++++++++++++++++++++++++------------ 1 file changed, 26 insertions(+), 12 deletions(-) diff --git a/src/JournalList.tsx b/src/JournalList.tsx index d5fffbc..bdbfa64 100644 --- a/src/JournalList.tsx +++ b/src/JournalList.tsx @@ -1,6 +1,8 @@ import * as React from 'react'; import { Link } from 'react-router-dom'; +import { List, ListItem } from 'material-ui/List'; + import { EteSyncContextType } from './EteSyncContext'; import * as EteSync from './api/EteSync'; @@ -38,26 +40,38 @@ export class JournalList extends React.Component { render() { const derived = this.props.etesync.encryptionKey; - const journals = this.state.journals.map((journal, idx) => { - let cryptoManager = new EteSync.CryptoManager(derived, journal.uid, journal.version); - let info = journal.getInfo(cryptoManager); - return ( -
  • + const journalMap = this.state.journals.reduce( + (ret, journal) => { + let cryptoManager = new EteSync.CryptoManager(derived, journal.uid, journal.version); + let info = journal.getInfo(cryptoManager); + ret[info.type] = ret[info.type] || []; + ret[info.type].push( - {info.displayName}: {info.type} ({journal.uid}) + + {info.displayName} ({journal.uid.slice(0, 5)}) + -
  • - ); - }); + ); + return ret; + }, + { CALENDAR: [], + ADDRESS_BOOK: []}); return (
    -
      - {journals} -
    +

    Address Books

    + + {journalMap.ADDRESS_BOOK} + + +

    Calendars

    + + {journalMap.CALENDAR} +
    ); } From 336a07e0656e4204f9841188feb9c11855d6322c Mon Sep 17 00:00:00 2001 From: Tom Hacohen Date: Mon, 4 Dec 2017 17:14:01 +0000 Subject: [PATCH 033/912] Remove the old welcome to react header. --- src/EteSyncContext.tsx | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/EteSyncContext.tsx b/src/EteSyncContext.tsx index f1da1e3..b204646 100644 --- a/src/EteSyncContext.tsx +++ b/src/EteSyncContext.tsx @@ -219,9 +219,6 @@ export class EteSyncContext extends React.Component { return (
    -
    -

    Welcome to React

    -
    Date: Mon, 4 Dec 2017 17:16:21 +0000 Subject: [PATCH 034/912] Improve the look of the journals page. --- src/JournalList.tsx | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/src/JournalList.tsx b/src/JournalList.tsx index bdbfa64..fd982f1 100644 --- a/src/JournalList.tsx +++ b/src/JournalList.tsx @@ -2,6 +2,7 @@ import * as React from 'react'; import { Link } from 'react-router-dom'; import { List, ListItem } from 'material-ui/List'; +import Paper from 'material-ui/Paper'; import { EteSyncContextType } from './EteSyncContext'; import * as EteSync from './api/EteSync'; @@ -61,8 +62,20 @@ export class JournalList extends React.Component { { CALENDAR: [], ADDRESS_BOOK: []}); + const styles = { + holder: { + margin: 'auto', + maxWidth: 400, + padding: 20, + }, + paper: { + padding: 20, + }, + }; + return ( -
    +
    +

    Address Books

    {journalMap.ADDRESS_BOOK} @@ -72,6 +85,7 @@ export class JournalList extends React.Component { {journalMap.CALENDAR} +
    ); } From 2a6220e6d40c4ff9af27cb111a586459e9e98238 Mon Sep 17 00:00:00 2001 From: Tom Hacohen Date: Mon, 4 Dec 2017 17:52:26 +0000 Subject: [PATCH 035/912] Make it possible to navigate between collection items and journal entries. --- src/JournalView.tsx | 42 +++++++++++++++++++++++++++++++----------- 1 file changed, 31 insertions(+), 11 deletions(-) diff --git a/src/JournalView.tsx b/src/JournalView.tsx index c63d42d..c913587 100644 --- a/src/JournalView.tsx +++ b/src/JournalView.tsx @@ -1,5 +1,7 @@ import * as React from 'react'; import { Route, Redirect } from 'react-router'; +import { Link } from 'react-router-dom'; +import { Tabs, Tab } from 'material-ui/Tabs'; import { EteSyncContextType } from './EteSyncContext'; import * as EteSync from './api/EteSync'; @@ -66,30 +68,48 @@ export class JournalView extends React.Component { return syncEntry; }); + let itemsTitle: string; + let itemsView: JSX.Element; + if (collectionInfo.type === 'CALENDAR') { + itemsView = ; + itemsTitle = 'Events'; + } else if (collectionInfo.type === 'ADDRESS_BOOK') { + itemsView = ; + itemsTitle = 'Contacts'; + } else { + itemsView =
    Unsupported type
    ; + itemsTitle = 'Items'; + } + return (
    + + } + /> + } + /> + } + render={() => } /> -

    Welcome to Journal!

    +

    {collectionInfo.displayName}

    - + render={() => { + return ; + } } /> { - if (collectionInfo.type === 'CALENDAR') { - return ; - } else if (collectionInfo.type === 'ADDRESS_BOOK') { - return ; - } else { - return
    Unsupported type
    ; - } + return itemsView; } } /> From fddf39244d49ab908b680a728522ca267bbd2e69 Mon Sep 17 00:00:00 2001 From: Tom Hacohen Date: Mon, 4 Dec 2017 19:18:13 +0000 Subject: [PATCH 036/912] Add a contact page. --- src/App.tsx | 2 +- src/JournalViewAddressBook.tsx | 41 ++++++++++++++-- src/JournalViewContact.tsx | 86 ++++++++++++++++++++++++++++++++++ src/ical.js.d.ts | 10 ++++ 4 files changed, 133 insertions(+), 6 deletions(-) create mode 100644 src/JournalViewContact.tsx diff --git a/src/App.tsx b/src/App.tsx index 747a5a6..4f36995 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -51,7 +51,7 @@ export const routeResolver = new RouteResolver({ items: { _base: 'items', _id: { - _base: ':entryUid', + _base: ':itemUid', }, }, entries: { diff --git a/src/JournalViewAddressBook.tsx b/src/JournalViewAddressBook.tsx index fd7612c..971967f 100644 --- a/src/JournalViewAddressBook.tsx +++ b/src/JournalViewAddressBook.tsx @@ -1,9 +1,15 @@ import * as React from 'react'; +import { Route, Switch } from 'react-router'; +import { List, ListItem } from 'material-ui/List'; +import { Link } from 'react-router-dom'; import * as ICAL from 'ical.js'; import * as EteSync from './api/EteSync'; +import { routeResolver } from './App'; +import { JournalViewContact } from './JournalViewContact'; + export class JournalViewAddressBook extends React.Component { static defaultProps = { prevUid: null, @@ -47,18 +53,43 @@ export class JournalViewAddressBook extends React.Component { } }); - let itemList = entries.map((entry, idx) => { + let itemList = entries.map((entry) => { + const uid = entry.getFirstPropertyValue('uid'); const name = entry.getFirstPropertyValue('fn'); return ( -
  • {name}
  • + + + ); }); return (
    -
      - {itemList} -
    + + ( + + {itemList} + + ) + } + /> + { + + return ( + + ); + }} + /> +
    ); } diff --git a/src/JournalViewContact.tsx b/src/JournalViewContact.tsx new file mode 100644 index 0000000..1885238 --- /dev/null +++ b/src/JournalViewContact.tsx @@ -0,0 +1,86 @@ +import * as React from 'react'; +import { List, ListItem } from 'material-ui/List'; +import Divider from 'material-ui/Divider'; +import CommunicationCall from 'material-ui/svg-icons/communication/call'; +import CommunicationChatBubble from 'material-ui/svg-icons/communication/chat-bubble'; +import CommunicationEmail from 'material-ui/svg-icons/communication/email'; +import { indigo500 } from 'material-ui/styles/colors'; + +import * as ICAL from 'ical.js'; + +export class JournalViewContact extends React.Component { + props: { + contact?: ICAL.Component, + }; + + render() { + if (this.props.contact === undefined) { + return (
    Loading
    ); + } + + const contact = this.props.contact; + const uid = contact.getFirstPropertyValue('uid'); + const name = contact.getFirstPropertyValue('fn'); + + const phoneNumbers = contact.getAllProperties('tel').map((prop, idx) => { + const json = prop.toJSON(); + const values = prop.getValues().map((val) => ( + } + rightIcon={} + primaryText={val} + secondaryText={json[1].type} + /> + )); + return values; + }); + + const emails = contact.getAllProperties('email').map((prop, idx) => { + const json = prop.toJSON(); + const values = prop.getValues().map((val) => ( + } + primaryText={val} + secondaryText={json[1].type} + /> + )); + return values; + }); + + const skips = ['tel', 'email', 'prodid', 'uid', 'fn', 'n', 'version', 'photo']; + const theRest = contact.getAllProperties().filter((prop) => ( + skips.indexOf(prop.name) === -1 + )).map((prop, idx) => { + const values = prop.getValues().map((_val) => { + const val = (_val instanceof String) ? _val : _val.toString(); + return ( + + ); + }); + return values; + }); + return ( +
    +

    {name} {uid}

    + + {phoneNumbers} + + + + {emails} + + + + {theRest} + +
    + ); + } +} diff --git a/src/ical.js.d.ts b/src/ical.js.d.ts index fc05263..e0f2d9d 100644 --- a/src/ical.js.d.ts +++ b/src/ical.js.d.ts @@ -8,6 +8,8 @@ declare module 'ical.js' { getFirstSubcomponent(name?: string): Component | null; getFirstPropertyValue(name?: string): string; + + getAllProperties(name?: string): Array; } class Event { @@ -17,4 +19,12 @@ declare module 'ical.js' { constructor(component?: Component | null, options?: {strictExceptions: boolean, exepctions: Array}); } + + class Property { + name: string; + type: string; + + getValues(): Array; + toJSON(): any; + } } From 154beee6d173283b9732b42fa7a5d75325659bbb Mon Sep 17 00:00:00 2001 From: Tom Hacohen Date: Mon, 4 Dec 2017 21:18:29 +0000 Subject: [PATCH 037/912] Fix input types for the login page. --- src/EteSyncContext.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/EteSyncContext.tsx b/src/EteSyncContext.tsx index b204646..a41e6a8 100644 --- a/src/EteSyncContext.tsx +++ b/src/EteSyncContext.tsx @@ -135,7 +135,7 @@ export class EteSyncContext extends React.Component { advancedSettings = (
    this.server = input as TextField} @@ -176,7 +176,7 @@ export class EteSyncContext extends React.Component {

    Please Log In

    this.username = input as TextField} From 635a41d41f152acbb1b2a63cbff833700858db73 Mon Sep 17 00:00:00 2001 From: Tom Hacohen Date: Mon, 4 Dec 2017 21:08:36 +0000 Subject: [PATCH 038/912] Add a calendar view. --- package.json | 3 ++ src/JournalViewCalendar.tsx | 57 +++++++++++++++++++++++------ src/ical.js.d.ts | 2 + yarn.lock | 73 +++++++++++++++++++++++++++++++++++-- 4 files changed, 121 insertions(+), 14 deletions(-) diff --git a/package.json b/package.json index b019e03..46e2d70 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,9 @@ "ical.js": "^1.2.2", "isomorphic-fetch": "^2.1.1", "material-ui": "^0.20.0", + "moment": "^2.19.3", "react": "^16.2.0", + "react-big-calendar": "^0.17.0", "react-dom": "^16.2.0", "react-router-dom": "^4.2.2", "react-scripts-ts": "2.8.0", @@ -25,6 +27,7 @@ "@types/material-ui": "^0.18.5", "@types/node": "^8.0.53", "@types/react": "^16.0.25", + "@types/react-big-calendar": "^0.15.0", "@types/react-dom": "^16.0.3", "@types/react-router": "^4.0.19", "@types/react-router-dom": "^4.2.3", diff --git a/src/JournalViewCalendar.tsx b/src/JournalViewCalendar.tsx index 5a97ff4..d31172b 100644 --- a/src/JournalViewCalendar.tsx +++ b/src/JournalViewCalendar.tsx @@ -1,19 +1,57 @@ import * as React from 'react'; +import BigCalendar from 'react-big-calendar'; +import 'react-big-calendar/lib/css/react-big-calendar.css'; +import * as moment from 'moment'; import * as ICAL from 'ical.js'; import * as EteSync from './api/EteSync'; +BigCalendar.momentLocalizer(moment); + +class EventWrapper { + event: ICAL.Event; + + constructor(event: ICAL.Event) { + this.event = event; + } + + get summary() { + return this.event.summary; + } + + get title() { + return this.summary; + } + + get start() { + return this.event.startDate.toJSDate(); + } + + get end() { + return this.event.endDate.toJSDate(); + } +} + export class JournalViewCalendar extends React.Component { static defaultProps = { prevUid: null, }; + state: { + currentDate?: Date; + }; + props: { journal: EteSync.Journal, entries: Array, }; + constructor(props: any) { + super(props); + this.state = {}; + } + render() { if (this.props.journal === undefined) { return (
    Loading
    ); @@ -39,7 +77,7 @@ export class JournalViewCalendar extends React.Component { } let entries = Array.from(items.values()).map((value) => ( - new ICAL.Event(value) + new EventWrapper(new ICAL.Event(value)) )).sort((a, b) => { if (a.summary < b.summary) { return -1; @@ -50,17 +88,14 @@ export class JournalViewCalendar extends React.Component { } }); - let itemList = entries.map((entry, idx) => { - return ( -
  • {entry.summary}
  • - ); - }); - return ( -
    -
      - {itemList} -
    +
    + { this.setState({currentDate}); }} + />
    ); } diff --git a/src/ical.js.d.ts b/src/ical.js.d.ts index e0f2d9d..7c004ca 100644 --- a/src/ical.js.d.ts +++ b/src/ical.js.d.ts @@ -15,6 +15,8 @@ declare module 'ical.js' { class Event { uid: string; summary: string; + startDate: any; + endDate: any; constructor(component?: Component | null, options?: {strictExceptions: boolean, exepctions: Array}); diff --git a/yarn.lock b/yarn.lock index 315756b..0416016 100644 --- a/yarn.lock +++ b/yarn.lock @@ -35,6 +35,12 @@ dependencies: "@types/react" "*" +"@types/react-big-calendar@^0.15.0": + version "0.15.0" + resolved "https://registry.yarnpkg.com/@types/react-big-calendar/-/react-big-calendar-0.15.0.tgz#3359ec60840ef8f51f506e1bb45c1eab0d337f01" + dependencies: + "@types/react" "*" + "@types/react-dom@^16.0.3": version "16.0.3" resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-16.0.3.tgz#8accad7eabdab4cca3e1a56f5ccb57de2da0ff64" @@ -879,6 +885,10 @@ clap@^1.0.9: dependencies: chalk "^1.1.3" +classnames@^2.1.3, classnames@^2.2.5: + version "2.2.5" + resolved "https://registry.yarnpkg.com/classnames/-/classnames-2.2.5.tgz#fb3801d453467649ef3603c7d61a02bd129bde6d" + clean-css@4.1.x: version "4.1.9" resolved "https://registry.yarnpkg.com/clean-css/-/clean-css-4.1.9.tgz#35cee8ae7687a49b98034f70de00c4edd3826301" @@ -1276,6 +1286,10 @@ dashdash@^1.12.0: dependencies: assert-plus "^1.0.0" +date-arithmetic@^3.0.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/date-arithmetic/-/date-arithmetic-3.1.0.tgz#1fcd03dbd504b9dbee2b9078c85a5f1c7d3cc2d3" + date-now@^0.1.4: version "0.1.4" resolved "https://registry.yarnpkg.com/date-now/-/date-now-0.1.4.tgz#eaf439fd4d4848ad74e5cc7dbef200672b9e345b" @@ -1420,7 +1434,7 @@ dom-converter@~0.1: dependencies: utila "~0.3" -dom-helpers@^3.2.0: +"dom-helpers@^2.3.0 || ^3.0.0", dom-helpers@^3.2.0, dom-helpers@^3.2.1: version "3.2.1" resolved "https://registry.yarnpkg.com/dom-helpers/-/dom-helpers-3.2.1.tgz#3203e07fed217bd1f424b019735582fc37b2825a" @@ -2512,7 +2526,7 @@ interpret@^1.0.0: version "1.1.0" resolved "https://registry.yarnpkg.com/interpret/-/interpret-1.1.0.tgz#7ed1b1410c6a0e0f78cf95d3b8440c63f78b8614" -invariant@^2.2.1, invariant@^2.2.2: +invariant@^2.1.0, invariant@^2.2.1, invariant@^2.2.2: version "2.2.2" resolved "https://registry.yarnpkg.com/invariant/-/invariant-2.2.2.tgz#9e1f56ac0acdb6bf303306f338be3b204ae60360" dependencies: @@ -3485,6 +3499,10 @@ mkdirp@0.5.x, "mkdirp@>=0.5 0", mkdirp@^0.5.0, mkdirp@^0.5.1, mkdirp@~0.5.0, mkd dependencies: minimist "0.0.8" +moment@^2.19.3: + version "2.19.3" + resolved "https://registry.yarnpkg.com/moment/-/moment-2.19.3.tgz#bdb99d270d6d7fda78cc0fbace855e27fe7da69f" + ms@2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8" @@ -4295,7 +4313,13 @@ promise@^7.1.1: dependencies: asap "~2.0.3" -prop-types@^15.5.4, prop-types@^15.5.6, prop-types@^15.5.7, prop-types@^15.6.0: +prop-types-extra@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/prop-types-extra/-/prop-types-extra-1.0.1.tgz#a57bd4810e82d27a3ff4317ecc1b4ad005f79a82" + dependencies: + warning "^3.0.0" + +prop-types@^15.5.10, prop-types@^15.5.4, prop-types@^15.5.6, prop-types@^15.5.7, prop-types@^15.5.8, prop-types@^15.6.0: version "15.6.0" resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.6.0.tgz#ceaf083022fc46b4a35f69e13ef75aed0d639856" dependencies: @@ -4413,6 +4437,21 @@ rc@^1.0.1, rc@^1.1.6, rc@^1.1.7: minimist "^1.2.0" strip-json-comments "~2.0.1" +react-big-calendar@^0.17.0: + version "0.17.0" + resolved "https://registry.yarnpkg.com/react-big-calendar/-/react-big-calendar-0.17.0.tgz#2c3cb0a660bf2ee22610453cbf123a886ffae6f1" + dependencies: + classnames "^2.1.3" + date-arithmetic "^3.0.0" + dom-helpers "^2.3.0 || ^3.0.0" + invariant "^2.1.0" + lodash "^4.17.4" + prop-types "^15.5.8" + react-overlays "^0.7.0" + react-prop-types "^0.4.0" + uncontrollable "^3.3.1 || ^4.0.0" + warning "^2.0.0" + react-dev-utils@^4.0.1: version "4.2.1" resolved "https://registry.yarnpkg.com/react-dev-utils/-/react-dev-utils-4.2.1.tgz#9f2763e7bafa1a1b9c52254d2a479deec280f111" @@ -4458,6 +4497,22 @@ react-event-listener@^0.5.1: prop-types "^15.6.0" warning "^3.0.0" +react-overlays@^0.7.0: + version "0.7.4" + resolved "https://registry.yarnpkg.com/react-overlays/-/react-overlays-0.7.4.tgz#ef2ec652c3444ab8aa014262b18f662068e56d5c" + dependencies: + classnames "^2.2.5" + dom-helpers "^3.2.1" + prop-types "^15.5.10" + prop-types-extra "^1.0.1" + warning "^3.0.0" + +react-prop-types@^0.4.0: + version "0.4.0" + resolved "https://registry.yarnpkg.com/react-prop-types/-/react-prop-types-0.4.0.tgz#f99b0bfb4006929c9af2051e7c1414a5c75b93d0" + dependencies: + warning "^3.0.0" + react-router-dom@^4.2.2: version "4.2.2" resolved "https://registry.yarnpkg.com/react-router-dom/-/react-router-dom-4.2.2.tgz#c8a81df3adc58bba8a76782e946cbd4eae649b8d" @@ -5546,6 +5601,12 @@ uid-number@^0.0.6: version "0.0.6" resolved "https://registry.yarnpkg.com/uid-number/-/uid-number-0.0.6.tgz#0ea10e8035e8eb5b8e4449f06da1c730663baa81" +"uncontrollable@^3.3.1 || ^4.0.0": + version "4.1.0" + resolved "https://registry.yarnpkg.com/uncontrollable/-/uncontrollable-4.1.0.tgz#e0358291252e1865222d90939b19f2f49f81c1a9" + dependencies: + invariant "^2.1.0" + uniq@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/uniq/-/uniq-1.0.1.tgz#b31c5ae8254844a3a8281541ce2b04b865a734ff" @@ -5696,6 +5757,12 @@ walker@~1.0.5: dependencies: makeerror "1.0.x" +warning@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/warning/-/warning-2.1.0.tgz#21220d9c63afc77a8c92111e011af705ce0c6901" + dependencies: + loose-envify "^1.0.0" + warning@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/warning/-/warning-3.0.0.tgz#32e5377cb572de4ab04753bdf8821c01ed605b7c" From 5b8c000d459c3aec993bf2b03a851898712bdb18 Mon Sep 17 00:00:00 2001 From: Tom Hacohen Date: Mon, 4 Dec 2017 21:41:03 +0000 Subject: [PATCH 039/912] Don't recreate views when switching tabs. --- src/JournalView.tsx | 25 ++++++++----------------- 1 file changed, 8 insertions(+), 17 deletions(-) diff --git a/src/JournalView.tsx b/src/JournalView.tsx index c913587..6bdcfaf 100644 --- a/src/JournalView.tsx +++ b/src/JournalView.tsx @@ -87,32 +87,23 @@ export class JournalView extends React.Component { } - /> + > +

    {collectionInfo.displayName}

    + {itemsView} +
    } - /> + > +

    {collectionInfo.displayName}

    + ; +
    } /> -

    {collectionInfo.displayName}

    - { - return ; - } - } - /> - { - return itemsView; - } - } - />
    ); } From 1a9202521d0a9bae86354ec256b1cc71f17a7f28 Mon Sep 17 00:00:00 2001 From: Tom Hacohen Date: Mon, 4 Dec 2017 21:55:47 +0000 Subject: [PATCH 040/912] Add in-app navigation links. --- src/App.tsx | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/App.tsx b/src/App.tsx index 4f36995..0a4f00f 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -7,6 +7,8 @@ import AppBar from 'material-ui/AppBar'; import Drawer from 'material-ui/Drawer'; import IconButton from 'material-ui/IconButton'; import { List, ListItem } from 'material-ui/List'; +import Subheader from 'material-ui/Subheader'; +import Divider from 'material-ui/Divider'; import ActionCode from 'material-ui/svg-icons/action/code'; import ActionHome from 'material-ui/svg-icons/action/home'; import ActionBugReport from 'material-ui/svg-icons/action/bug-report'; @@ -106,6 +108,9 @@ class App extends React.Component {
    + } href={routeResolver.getRoute('home')} /> + + External Links } href={C.homePage} /> } href={C.faq} /> } href={C.sourceCode} /> From 1bf6314ac35bc93bce681f2e44ca7e7cbdd4b224 Mon Sep 17 00:00:00 2001 From: Tom Hacohen Date: Mon, 4 Dec 2017 22:12:32 +0000 Subject: [PATCH 041/912] EteSync: fix the sync entry action constant for Delete. --- src/api/EteSync.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/api/EteSync.tsx b/src/api/EteSync.tsx index 4fc43bf..32a5a42 100644 --- a/src/api/EteSync.tsx +++ b/src/api/EteSync.tsx @@ -165,7 +165,7 @@ export class Journal extends BaseJournal { export enum SyncEntryAction { Add = 'ADD', - Delete = 'DEL', + Delete = 'DELETE', Change = 'CHANGE', } From b0741978d0288892d012c9d169469936995116e2 Mon Sep 17 00:00:00 2001 From: Tom Hacohen Date: Mon, 4 Dec 2017 22:14:02 +0000 Subject: [PATCH 042/912] Journal list: improve look. --- src/JournalViewEntries.tsx | 43 +++++++++++++++++++++++++++++++++----- 1 file changed, 38 insertions(+), 5 deletions(-) diff --git a/src/JournalViewEntries.tsx b/src/JournalViewEntries.tsx index 656d34f..e4bfe42 100644 --- a/src/JournalViewEntries.tsx +++ b/src/JournalViewEntries.tsx @@ -1,4 +1,8 @@ import * as React from 'react'; +import { List, ListItem } from 'material-ui/List'; +import IconAdd from 'material-ui/svg-icons/content/add'; +import IconDelete from 'material-ui/svg-icons/action/delete'; +import IconEdit from 'material-ui/svg-icons/editor/mode-edit'; import * as ICAL from 'ical.js'; @@ -22,24 +26,53 @@ export class JournalViewEntries extends React.Component { const entries = this.props.entries.map((syncEntry, idx) => { const comp = new ICAL.Component(ICAL.parse(syncEntry.content)); + let icon; + if (syncEntry.action === EteSync.SyncEntryAction.Add) { + icon = (); + } else if (syncEntry.action === EteSync.SyncEntryAction.Change) { + icon = (); + } else if (syncEntry.action === EteSync.SyncEntryAction.Delete) { + icon = (); + } + if (comp.name === 'vcalendar') { const vevent = new ICAL.Event(comp.getFirstSubcomponent('vevent')); - return (
  • {syncEntry.action}: {vevent.summary} ({vevent.uid})
  • ); + return ( + + ); } else if (comp.name === 'vcard') { const vcard = comp; const name = vcard.getFirstPropertyValue('fn'); const uid = vcard.getFirstPropertyValue('uid'); - return (
  • {syncEntry.action}: {name} ({uid})
  • ); + return ( + + ); } else { - return (
  • {syncEntry.action}: {syncEntry.content}
  • ); + return ( + + ); } }).reverse(); return (
    -
      + {entries} -
    +
    ); } From df4ea83208aa51788f82ea7c7fd18c16099b80f9 Mon Sep 17 00:00:00 2001 From: Tom Hacohen Date: Mon, 4 Dec 2017 22:16:15 +0000 Subject: [PATCH 043/912] Don't change history when switching tabs. --- src/JournalView.tsx | 11 ----------- src/JournalViewAddressBook.tsx | 2 +- 2 files changed, 1 insertion(+), 12 deletions(-) diff --git a/src/JournalView.tsx b/src/JournalView.tsx index 6bdcfaf..d6a94f5 100644 --- a/src/JournalView.tsx +++ b/src/JournalView.tsx @@ -1,13 +1,9 @@ import * as React from 'react'; -import { Route, Redirect } from 'react-router'; -import { Link } from 'react-router-dom'; import { Tabs, Tab } from 'material-ui/Tabs'; import { EteSyncContextType } from './EteSyncContext'; import * as EteSync from './api/EteSync'; -import { routeResolver } from './App'; - import { JournalViewEntries } from './JournalViewEntries'; import { JournalViewAddressBook } from './JournalViewAddressBook'; import { JournalViewCalendar } from './JournalViewCalendar'; @@ -86,24 +82,17 @@ export class JournalView extends React.Component { } >

    {collectionInfo.displayName}

    {itemsView}
    } >

    {collectionInfo.displayName}

    ;
    - } - />
    ); } diff --git a/src/JournalViewAddressBook.tsx b/src/JournalViewAddressBook.tsx index 971967f..c140bfd 100644 --- a/src/JournalViewAddressBook.tsx +++ b/src/JournalViewAddressBook.tsx @@ -70,7 +70,7 @@ export class JournalViewAddressBook extends React.Component {
    ( From 00d8b43cc540ca74f98f921b689155db6cb852fc Mon Sep 17 00:00:00 2001 From: Tom Hacohen Date: Mon, 4 Dec 2017 22:26:30 +0000 Subject: [PATCH 044/912] Login: change login form to be a controlled component. I thought it would be easier to use an uncontrolled component, but it fails when loading fails. The form just clears. This fixes it. --- src/EteSyncContext.tsx | 49 ++++++++++++++++++++++++++++++------------ 1 file changed, 35 insertions(+), 14 deletions(-) diff --git a/src/EteSyncContext.tsx b/src/EteSyncContext.tsx index a41e6a8..8b9f740 100644 --- a/src/EteSyncContext.tsx +++ b/src/EteSyncContext.tsx @@ -36,17 +36,17 @@ interface FormErrors { } export class EteSyncContext extends React.Component { - server: TextField; - username: TextField; - password: TextField; - encryptionPassword: TextField; - state: { context?: EteSyncContextType; loadState: LoadState; showAdvanced?: boolean; error?: Error; errors: FormErrors; + + server: string; + username: string; + password: string; + encryptionPassword: string; }; constructor(props: any) { @@ -54,9 +54,14 @@ export class EteSyncContext extends React.Component { this.state = { loadState: LoadState.Initial, errors: {}, + server: '', + username: '', + password: '', + encryptionPassword: '', }; this.generateEncryption = this.generateEncryption.bind(this); this.toggleAdvancedSettings = this.toggleAdvancedSettings.bind(this); + this.handleInputChange = this.handleInputChange.bind(this); const contextStr = sessionStorage.getItem(CONTEXT_SESSION_KEY); @@ -70,15 +75,23 @@ export class EteSyncContext extends React.Component { } } + handleInputChange(event: any) { + const name = event.target.name; + const value = event.target.value; + this.setState({ + [name]: value + }); + } + generateEncryption(e: any) { e.preventDefault(); - const server = this.state.showAdvanced ? this.server.getValue() : C.serviceApiBase; + const server = this.state.showAdvanced ? this.state.server : C.serviceApiBase; let authenticator = new EteSync.Authenticator(server); - const username = this.username.getValue(); - const password = this.password.getValue(); - const encryptionPassword = this.encryptionPassword.getValue(); + const username = this.state.username; + const password = this.state.password; + const encryptionPassword = this.state.encryptionPassword; let errors: FormErrors = {}; const fieldRequired = 'This field is required!'; @@ -138,7 +151,9 @@ export class EteSyncContext extends React.Component { type="url" errorText={this.state.errors.errorServer} floatingLabelText="Server" - ref={(input) => this.server = input as TextField} + name="server" + value={this.state.server} + onChange={this.handleInputChange} />
    @@ -179,13 +194,17 @@ export class EteSyncContext extends React.Component { type="email" errorText={this.state.errors.errorEmail} floatingLabelText="Email" - ref={(input) => this.username = input as TextField} + name="username" + value={this.state.username} + onChange={this.handleInputChange} /> this.password = input as TextField} + name="password" + value={this.state.password} + onChange={this.handleInputChange} />
    Forgot password? @@ -194,7 +213,9 @@ export class EteSyncContext extends React.Component { type="password" errorText={this.state.errors.errorEncryptionPassword} floatingLabelText="Encryption Password" - ref={(input) => this.encryptionPassword = input as TextField} + name="encryptionPassword" + value={this.state.encryptionPassword} + onChange={this.handleInputChange} /> loading
    ); + return (
    Loading
    ); } let context: EteSyncContextType = this.state.context; From fe6490a098850a5f5c2996688bb9c637e0388a3b Mon Sep 17 00:00:00 2001 From: Tom Hacohen Date: Mon, 4 Dec 2017 23:18:06 +0000 Subject: [PATCH 045/912] Show raw journal items when clicked. --- src/JournalViewEntries.tsx | 79 ++++++++++++++++++++++++++------------ src/api/EteSync.tsx | 6 ++- 2 files changed, 58 insertions(+), 27 deletions(-) diff --git a/src/JournalViewEntries.tsx b/src/JournalViewEntries.tsx index e4bfe42..6e0d174 100644 --- a/src/JournalViewEntries.tsx +++ b/src/JournalViewEntries.tsx @@ -1,5 +1,7 @@ import * as React from 'react'; import { List, ListItem } from 'material-ui/List'; +import Dialog from 'material-ui/Dialog'; +import FlatButton from 'material-ui/FlatButton'; import IconAdd from 'material-ui/svg-icons/content/add'; import IconDelete from 'material-ui/svg-icons/action/delete'; import IconEdit from 'material-ui/svg-icons/editor/mode-edit'; @@ -13,11 +15,20 @@ export class JournalViewEntries extends React.Component { prevUid: null, }; + state: { + dialog?: string; + }; + props: { journal: EteSync.Journal, entries: Array, }; + constructor(props: any) { + super(props); + this.state = {}; + } + render() { if (this.props.journal === undefined) { return (
    Loading
    ); @@ -35,41 +46,59 @@ export class JournalViewEntries extends React.Component { icon = (); } + let name; + let uid; if (comp.name === 'vcalendar') { const vevent = new ICAL.Event(comp.getFirstSubcomponent('vevent')); - return ( - - ); + name = vevent.summary; + uid = vevent.uid; } else if (comp.name === 'vcard') { const vcard = comp; - const name = vcard.getFirstPropertyValue('fn'); - const uid = vcard.getFirstPropertyValue('uid'); - return ( - - ); + name = vcard.getFirstPropertyValue('fn'); + uid = vcard.getFirstPropertyValue('uid'); } else { - return ( - - ); + name = 'Error processing entry'; + uid = ''; } + return ( + { + this.setState({ + dialog: syncEntry.content + }); + }} + /> + ); }).reverse(); + const actions = [( + { + this.setState({dialog: undefined}); + }} + /> + ), + ]; return (
    + { + this.setState({dialog: undefined}); + }} + > +
    {this.state.dialog}
    +
    {entries} diff --git a/src/api/EteSync.tsx b/src/api/EteSync.tsx index 32a5a42..d2b41ec 100644 --- a/src/api/EteSync.tsx +++ b/src/api/EteSync.tsx @@ -170,11 +170,13 @@ export enum SyncEntryAction { } export class SyncEntry { + uid?: string; action: SyncEntryAction; content: string; - constructor(json?: any) { + constructor(json?: any, uid?: string) { CastJson(json, this); + this.uid = uid; } } @@ -195,7 +197,7 @@ export class Entry extends BaseJournal { this._content = JSON.parse(cryptoManager.decrypt(this._encrypted)); } - return new SyncEntry(this._content); + return new SyncEntry(this._content, this.uid); } verify(cryptoManager: CryptoManager, prevUid: string | null) { From 36cf88aaf2ed6ef2ad1d889997420b679404d486 Mon Sep 17 00:00:00 2001 From: Tom Hacohen Date: Mon, 4 Dec 2017 23:23:56 +0000 Subject: [PATCH 046/912] Fix the main navigation link. --- src/App.tsx | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/src/App.tsx b/src/App.tsx index 0a4f00f..a11e0fb 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,5 +1,5 @@ import * as React from 'react'; -import { HashRouter } from 'react-router-dom'; +import { HashRouter, NavLink } from 'react-router-dom'; import getMuiTheme from 'material-ui/styles/getMuiTheme'; import MuiThemeProvider from 'material-ui/styles/MuiThemeProvider'; import { amber500, amber700, lightBlue500, darkBlack, white } from 'material-ui/styles/colors'; @@ -90,6 +90,7 @@ class App extends React.Component { render() { return ( +
    - } href={routeResolver.getRoute('home')} /> + + } onClick={this.closeDrawer} /> + External Links } href={C.homePage} /> @@ -118,10 +124,9 @@ class App extends React.Component { - - - +
    + ); } From 808c5111b8fe5a012cf007c2ce74cacc51a9022d Mon Sep 17 00:00:00 2001 From: Tom Hacohen Date: Tue, 5 Dec 2017 17:07:53 +0000 Subject: [PATCH 047/912] Update README about the hosted client. --- README.md | 41 ++++++++++++++++++++++++++++++++--------- 1 file changed, 32 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index 1de9560..eacf69a 100644 --- a/README.md +++ b/README.md @@ -1,17 +1,40 @@ An EteSync web client. Use EteSync from the browser -# Status +# Usage -This project is not ready just yet, but it'll hopefully be soon. +**Note:** This is still in an early stage. It should perfectly safe to use (though +plase refer to the warning at the bottom) but you should expect bugs/bad design. -After it's ready you'll be able to run it locally and just use a full EteSync -client from the browser! +A live isntance is available on: https://client.etesync.com -We will also have a hosted version of this, but it's not as secure as running -your own. More info [here](https://www.etesync.com/faq/#web-client). +Please be advised that while it's probably safe enough to use the hosted client +in many cases, it's generally not preferable. It's recommended that you use signed +releases which's signature you manually verify and are run locally! + + +More info is available on the [FAQ](https://www.etesync.com/faq/#web-client). + +## Running your own + +You can either self-host your own client to be served from your own server, or +better yet, just run an instance locally. + +First make sure you have `yarn` install. + +Then clone this repository `yarn`, run `yarn` and wait until all of the deps are installed. + +Then it's recommended you run `yarn build` to build a production ready client you should serve +(even if run locally!) and then just serve the `build` directory from a web server. +You could for exapmle use the python built-in web server by runnig `python3 -m http.server` from +the build directory. + +Alternatively, you can run the debug server just to verify everything works. To do that, +run `yarn start`. # Important! -There are some rough edges at the moment. For example, it uses a static IV -instead of a secure random generated one! Please DO NOT use this for any real -data and encryption keys until this warning is removed. +There are some rough edges at the moment, this project is still in early alpha! + +While it's OK to use it for **accessing** your data, do **NOT** use it to create +new contacts/calendars while this notice is here! It uses a static IV for all +encryption operations at the moment, which is very bad! From 55f595d52abcc07e5a83fb5d9d000ba935f9abdd Mon Sep 17 00:00:00 2001 From: Tom Hacohen Date: Tue, 5 Dec 2017 10:11:29 +0000 Subject: [PATCH 048/912] Start using react fragments. --- src/JournalView.tsx | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/JournalView.tsx b/src/JournalView.tsx index d6a94f5..1465e47 100644 --- a/src/JournalView.tsx +++ b/src/JournalView.tsx @@ -1,4 +1,5 @@ import * as React from 'react'; +const Fragment = (React as any).Fragment; import { Tabs, Tab } from 'material-ui/Tabs'; import { EteSyncContextType } from './EteSyncContext'; @@ -78,7 +79,7 @@ export class JournalView extends React.Component { } return ( -
    + ; -
    + ); } } From 94c69164470312435781580b3ea3f6cf99a47a71 Mon Sep 17 00:00:00 2001 From: Tom Hacohen Date: Tue, 5 Dec 2017 13:09:54 +0000 Subject: [PATCH 049/912] Add a basic redux store. --- package.json | 7 +++ src/EteSyncContext.tsx | 110 +++++++++++++++++++-------------------- src/index.tsx | 10 +++- src/redux-persist.d.ts | 3 ++ src/store.tsx | 113 +++++++++++++++++++++++++++++++++++++++++ yarn.lock | 63 +++++++++++++++++++++-- 6 files changed, 243 insertions(+), 63 deletions(-) create mode 100644 src/redux-persist.d.ts create mode 100644 src/store.tsx diff --git a/package.json b/package.json index 46e2d70..91bb703 100644 --- a/package.json +++ b/package.json @@ -10,8 +10,13 @@ "react": "^16.2.0", "react-big-calendar": "^0.17.0", "react-dom": "^16.2.0", + "react-redux": "^5.0.6", "react-router-dom": "^4.2.2", "react-scripts-ts": "2.8.0", + "redux": "^3.7.2", + "redux-logger": "^3.0.6", + "redux-persist": "^5.4.0", + "redux-thunk": "^2.2.0", "sjcl": "git+https://github.com/etesync/sjcl", "urijs": "^1.16.1" }, @@ -29,8 +34,10 @@ "@types/react": "^16.0.25", "@types/react-big-calendar": "^0.15.0", "@types/react-dom": "^16.0.3", + "@types/react-redux": "^5.0.14", "@types/react-router": "^4.0.19", "@types/react-router-dom": "^4.2.3", + "@types/redux-logger": "^3.0.5", "@types/sjcl": "^1.0.28", "@types/urijs": "^1.15.34" } diff --git a/src/EteSyncContext.tsx b/src/EteSyncContext.tsx index 8b9f740..480d4b6 100644 --- a/src/EteSyncContext.tsx +++ b/src/EteSyncContext.tsx @@ -1,5 +1,6 @@ import * as React from 'react'; -import { Switch, Route, Redirect } from 'react-router'; +import { connect } from 'react-redux'; +import { Switch, Route, Redirect, withRouter } from 'react-router'; import Paper from 'material-ui/Paper'; import RaisedButton from 'material-ui/RaisedButton'; import TextField from 'material-ui/TextField'; @@ -11,17 +12,10 @@ import { JournalView } from './JournalView'; import * as EteSync from './api/EteSync'; import { routeResolver, getPalette } from './App'; +import * as store from './store'; import * as C from './Constants'; -const CONTEXT_SESSION_KEY = 'EteSyncContext'; - -enum LoadState { - Initial = 'INIT', - Working = 'WORKING', - Done = 'DONE', -} - export interface EteSyncContextType { serviceApiUrl: string; credentials: EteSync.Credentials; @@ -35,12 +29,35 @@ interface FormErrors { errorServer?: string; } -export class EteSyncContext extends React.Component { +function fetchCredentials(username: string, password: string, encryptionPassword: string, server: string) { + const authenticator = new EteSync.Authenticator(server); + + return (dispatch: any) => { + dispatch(store.credentialsRequest()); + + authenticator.getAuthToken(username, password).then( + (authToken) => { + const credentials = new EteSync.Credentials(username, authToken); + const derived = EteSync.deriveKey(username, encryptionPassword); + + const context = { + serviceApiUrl: server, + credentials, + encryptionKey: derived, + }; + + dispatch(store.credentialsSuccess(context)); + }, + (error) => { + dispatch(store.credentialsFailure(error)); + } + ); + }; +} + +export class EteSyncContextInner extends React.Component { state: { - context?: EteSyncContextType; - loadState: LoadState; showAdvanced?: boolean; - error?: Error; errors: FormErrors; server: string; @@ -49,10 +66,13 @@ export class EteSyncContext extends React.Component { encryptionPassword: string; }; + props: { + credentials: store.CredentialsType; + }; + constructor(props: any) { super(props); this.state = { - loadState: LoadState.Initial, errors: {}, server: '', username: '', @@ -62,17 +82,6 @@ export class EteSyncContext extends React.Component { this.generateEncryption = this.generateEncryption.bind(this); this.toggleAdvancedSettings = this.toggleAdvancedSettings.bind(this); this.handleInputChange = this.handleInputChange.bind(this); - - const contextStr = sessionStorage.getItem(CONTEXT_SESSION_KEY); - - if (contextStr !== null) { - const context: EteSyncContextType = JSON.parse(contextStr); - - this.state = Object.assign({}, this.state, { - loadState: LoadState.Done, - context - }); - } } handleInputChange(event: any) { @@ -87,8 +96,6 @@ export class EteSyncContext extends React.Component { e.preventDefault(); const server = this.state.showAdvanced ? this.state.server : C.serviceApiBase; - let authenticator = new EteSync.Authenticator(server); - const username = this.state.username; const password = this.state.password; const encryptionPassword = this.state.encryptionPassword; @@ -109,32 +116,7 @@ export class EteSyncContext extends React.Component { return; } - this.setState({ - loadState: LoadState.Working - }); - - authenticator.getAuthToken(username, password).then((authToken) => { - const credentials = new EteSync.Credentials(username, authToken); - const derived = EteSync.deriveKey(username, encryptionPassword); - - const context = { - serviceApiUrl: server, - credentials, - encryptionKey: derived, - }; - - sessionStorage.setItem(CONTEXT_SESSION_KEY, JSON.stringify(context)); - - this.setState({ - loadState: LoadState.Done, - context - }); - }).catch((error) => { - this.setState({ - loadState: LoadState.Initial, - error - }); - }); + store.store.dispatch(fetchCredentials(username, password, encryptionPassword, server)); } toggleAdvancedSettings() { @@ -142,7 +124,10 @@ export class EteSyncContext extends React.Component { } render() { - if (this.state.loadState === LoadState.Initial) { + if (((this.props.credentials.status === store.FetchStatus.Initial) && + (this.props.credentials.credentials === undefined)) || + (this.props.credentials.status === store.FetchStatus.Failure)) { + let advancedSettings = null; if (this.state.showAdvanced) { advancedSettings = ( @@ -187,7 +172,7 @@ export class EteSyncContext extends React.Component { return (
    - {(this.state.error !== undefined) && (
    Error! {this.state.error.message}
    )} + {(this.props.credentials.error !== undefined) && (
    Error! {this.props.credentials.error.message}
    )}

    Please Log In

    ); - } else if ((this.state.context === undefined) || - (this.state.loadState === LoadState.Working)) { + } else if (this.props.credentials.status === store.FetchStatus.Request) { return (
    Loading
    ); } - let context: EteSyncContextType = this.state.context; + let context = this.props.credentials.credentials as store.CredentialsData; return (
    @@ -260,3 +244,13 @@ export class EteSyncContext extends React.Component { ); } } + +const mapStateToProps = (state: store.StoreState) => { + return { + credentials: state.credentials, + }; +}; + +export const EteSyncContext = withRouter(connect( + mapStateToProps +)(EteSyncContextInner)); diff --git a/src/index.tsx b/src/index.tsx index 1c66245..6dbd0d0 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -1,11 +1,19 @@ import * as React from 'react'; import * as ReactDOM from 'react-dom'; +import { Provider } from 'react-redux'; +import { PersistGate } from 'redux-persist/es/integration/react'; import App from './App'; import registerServiceWorker from './registerServiceWorker'; import './index.css'; +import { store, persistor } from './store'; + ReactDOM.render( - , + + + + + , document.getElementById('root') as HTMLElement ); registerServiceWorker(); diff --git a/src/redux-persist.d.ts b/src/redux-persist.d.ts new file mode 100644 index 0000000..6d6e080 --- /dev/null +++ b/src/redux-persist.d.ts @@ -0,0 +1,3 @@ +declare module 'redux-persist'; +declare module 'redux-persist/lib/storage/session'; +declare module 'redux-persist/es/integration/react'; diff --git a/src/store.tsx b/src/store.tsx new file mode 100644 index 0000000..3c5b26a --- /dev/null +++ b/src/store.tsx @@ -0,0 +1,113 @@ +import { createStore, combineReducers, applyMiddleware } from 'redux'; +import { persistReducer, persistStore } from 'redux-persist'; +import session from 'redux-persist/lib/storage/session'; +import thunkMiddleware from 'redux-thunk'; +import { createLogger } from 'redux-logger'; + +import * as EteSync from './api/EteSync'; + +const loggerMiddleware = createLogger(); + +enum Actions { + FETCH_CREDENTIALS = 'FETCH_CREDENTIALS', +} + +export enum FetchStatus { + Initial = 'INITIAL', + Request = 'REQUEST', + Failure = 'FAILURE', + Success = 'SUCCESS', +} + +export interface CredentialsData { + serviceApiUrl: string; + credentials: EteSync.Credentials; + encryptionKey: string; +} + +export interface CredentialsType { + status: FetchStatus; + error?: Error; + credentials?: CredentialsData; +} + +export interface StoreState { + fetchCount: number; + credentials: CredentialsData; +} + +export function credentialsSuccess(creds: CredentialsData) { + return { + type: Actions.FETCH_CREDENTIALS, + status: FetchStatus.Success, + credentials: creds, + }; +} + +export function credentialsRequest() { + return { + type: Actions.FETCH_CREDENTIALS, + status: FetchStatus.Request, + }; +} + +export function credentialsFailure(error: Error) { + return { + type: Actions.FETCH_CREDENTIALS, + status: FetchStatus.Failure, + error + }; +} + +function credentials(state: CredentialsType = {status: FetchStatus.Initial}, action: any) { + switch (action.type) { + case Actions.FETCH_CREDENTIALS: + if (action.status === FetchStatus.Success) { + return { + status: action.status, + credentials: action.credentials, + }; + } else { + return { + status: action.status, + }; + } + default: + return state; + } +} + +function fetchCount(state: number = 0, action: any) { + // FIXME: Make it automatic by action properties. + switch (action.type) { + case Actions.FETCH_CREDENTIALS: + if (action.status === FetchStatus.Request) { + return state + 1; + } else { + return state - 1; + } + default: + return state; + } +} + +const credentialsPersistConfig = { + key: 'credentials', + storage: session, + whitelist: ['credentials'], +}; + +const reducers = combineReducers({ + fetchCount, + credentials: persistReducer(credentialsPersistConfig, credentials), +}); + +export const store = createStore( + reducers, + applyMiddleware( + thunkMiddleware, + loggerMiddleware + ) +); + +export const persistor = persistStore(store); diff --git a/yarn.lock b/yarn.lock index 0416016..97063d6 100644 --- a/yarn.lock +++ b/yarn.lock @@ -48,6 +48,13 @@ "@types/node" "*" "@types/react" "*" +"@types/react-redux@^5.0.14": + version "5.0.14" + resolved "https://registry.yarnpkg.com/@types/react-redux/-/react-redux-5.0.14.tgz#f3fc30dcbb2d20455a714f591cc27f77b4df09bb" + dependencies: + "@types/react" "*" + redux "^3.6.0" + "@types/react-router-dom@^4.2.3": version "4.2.3" resolved "https://registry.yarnpkg.com/@types/react-router-dom/-/react-router-dom-4.2.3.tgz#06e0b67ff536adc0681dffdbe592ae91fb85887d" @@ -67,6 +74,12 @@ version "16.0.25" resolved "https://registry.yarnpkg.com/@types/react/-/react-16.0.25.tgz#bf696b83fe480c5e0eff4335ee39ebc95884a1ed" +"@types/redux-logger@^3.0.5": + version "3.0.5" + resolved "https://registry.yarnpkg.com/@types/redux-logger/-/redux-logger-3.0.5.tgz#d1a02758f90845899cd304aa0912daeba2028eb6" + dependencies: + redux "^3.6.0" + "@types/sjcl@^1.0.28": version "1.0.28" resolved "https://registry.yarnpkg.com/@types/sjcl/-/sjcl-1.0.28.tgz#4693eb6943e385e844a70fb25b4699db286c7214" @@ -1310,6 +1323,10 @@ decamelize@^1.0.0, decamelize@^1.1.1, decamelize@^1.1.2: version "1.2.0" resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-1.2.0.tgz#f6534d15148269b20352e7bee26f501f9a191290" +deep-diff@^0.3.5: + version "0.3.8" + resolved "https://registry.yarnpkg.com/deep-diff/-/deep-diff-0.3.8.tgz#c01de63efb0eec9798801d40c7e0dae25b582c84" + deep-equal@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/deep-equal/-/deep-equal-1.0.1.tgz#f5d260292b660e084eff4cdbc9f08ad3247448b5" @@ -2297,7 +2314,7 @@ hoek@4.x.x: version "4.2.0" resolved "https://registry.yarnpkg.com/hoek/-/hoek-4.2.0.tgz#72d9d0754f7fe25ca2d01ad8f8f9a9449a89526d" -hoist-non-react-statics@^2.3.0, hoist-non-react-statics@^2.3.1: +hoist-non-react-statics@^2.2.1, hoist-non-react-statics@^2.3.0, hoist-non-react-statics@^2.3.1: version "2.3.1" resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-2.3.1.tgz#343db84c6018c650778898240135a1420ee22ce0" @@ -2526,7 +2543,7 @@ interpret@^1.0.0: version "1.1.0" resolved "https://registry.yarnpkg.com/interpret/-/interpret-1.1.0.tgz#7ed1b1410c6a0e0f78cf95d3b8440c63f78b8614" -invariant@^2.1.0, invariant@^2.2.1, invariant@^2.2.2: +invariant@^2.0.0, invariant@^2.1.0, invariant@^2.2.1, invariant@^2.2.2: version "2.2.2" resolved "https://registry.yarnpkg.com/invariant/-/invariant-2.2.2.tgz#9e1f56ac0acdb6bf303306f338be3b204ae60360" dependencies: @@ -3240,6 +3257,10 @@ locate-path@^2.0.0: p-locate "^2.0.0" path-exists "^3.0.0" +lodash-es@^4.2.0, lodash-es@^4.2.1: + version "4.17.4" + resolved "https://registry.yarnpkg.com/lodash-es/-/lodash-es-4.17.4.tgz#dcc1d7552e150a0640073ba9cb31d70f032950e7" + lodash._reinterpolate@~3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/lodash._reinterpolate/-/lodash._reinterpolate-3.0.0.tgz#0ccf2d89166af03b3663c796538b75ac6e114d9d" @@ -3281,7 +3302,7 @@ lodash.uniq@^4.5.0: version "4.5.0" resolved "https://registry.yarnpkg.com/lodash.uniq/-/lodash.uniq-4.5.0.tgz#d0225373aeb652adc1bc82e4945339a842754773" -"lodash@>=3.5 <5", lodash@^4.14.0, lodash@^4.17.2, lodash@^4.17.3, lodash@^4.17.4, lodash@^4.3.0: +"lodash@>=3.5 <5", lodash@^4.14.0, lodash@^4.17.2, lodash@^4.17.3, lodash@^4.17.4, lodash@^4.2.0, lodash@^4.2.1, lodash@^4.3.0: version "4.17.4" resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.4.tgz#78203a4d1c328ae1d86dca6460e369b57f4055ae" @@ -4513,6 +4534,17 @@ react-prop-types@^0.4.0: dependencies: warning "^3.0.0" +react-redux@^5.0.6: + version "5.0.6" + resolved "https://registry.yarnpkg.com/react-redux/-/react-redux-5.0.6.tgz#23ed3a4f986359d68b5212eaaa681e60d6574946" + dependencies: + hoist-non-react-statics "^2.2.1" + invariant "^2.0.0" + lodash "^4.2.0" + lodash-es "^4.2.0" + loose-envify "^1.1.0" + prop-types "^15.5.10" + react-router-dom@^4.2.2: version "4.2.2" resolved "https://registry.yarnpkg.com/react-router-dom/-/react-router-dom-4.2.2.tgz#c8a81df3adc58bba8a76782e946cbd4eae649b8d" @@ -4694,6 +4726,29 @@ reduce-function-call@^1.0.1: dependencies: balanced-match "^0.4.2" +redux-logger@^3.0.6: + version "3.0.6" + resolved "https://registry.yarnpkg.com/redux-logger/-/redux-logger-3.0.6.tgz#f7555966f3098f3c88604c449cf0baf5778274bf" + dependencies: + deep-diff "^0.3.5" + +redux-persist@^5.4.0: + version "5.4.0" + resolved "https://registry.yarnpkg.com/redux-persist/-/redux-persist-5.4.0.tgz#a1062313546a9d4ca6f9271464d18f736e8ca394" + +redux-thunk@^2.2.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/redux-thunk/-/redux-thunk-2.2.0.tgz#e615a16e16b47a19a515766133d1e3e99b7852e5" + +redux@^3.6.0, redux@^3.7.2: + version "3.7.2" + resolved "https://registry.yarnpkg.com/redux/-/redux-3.7.2.tgz#06b73123215901d25d065be342eb026bc1c8537b" + dependencies: + lodash "^4.2.1" + lodash-es "^4.2.1" + loose-envify "^1.1.0" + symbol-observable "^1.0.3" + regenerate@^1.2.1: version "1.3.3" resolved "https://registry.yarnpkg.com/regenerate/-/regenerate-1.3.3.tgz#0c336d3980553d755c39b586ae3b20aa49c82b7f" @@ -5355,7 +5410,7 @@ sw-toolbox@^3.4.0: path-to-regexp "^1.0.1" serviceworker-cache-polyfill "^4.0.0" -symbol-observable@^1.0.4: +symbol-observable@^1.0.3, symbol-observable@^1.0.4: version "1.1.0" resolved "https://registry.yarnpkg.com/symbol-observable/-/symbol-observable-1.1.0.tgz#5c68fd8d54115d9dfb72a84720549222e8db9b32" From 3c3ffb15d38b41c4922b71e49e5e8c77317e6c73 Mon Sep 17 00:00:00 2001 From: Tom Hacohen Date: Tue, 5 Dec 2017 14:52:43 +0000 Subject: [PATCH 050/912] Implement logout and workaround an issue with redux-persist not persisting. For some reason it doesn't persist when credentials === undefined. --- src/App.tsx | 24 +++++++++++++++++++- src/EteSyncContext.tsx | 2 +- src/store.tsx | 50 +++++++++++++++++++++++++++++------------- 3 files changed, 59 insertions(+), 17 deletions(-) diff --git a/src/App.tsx b/src/App.tsx index a11e0fb..e22e3c7 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,4 +1,5 @@ import * as React from 'react'; +import { connect } from 'react-redux'; import { HashRouter, NavLink } from 'react-router-dom'; import getMuiTheme from 'material-ui/styles/getMuiTheme'; import MuiThemeProvider from 'material-ui/styles/MuiThemeProvider'; @@ -13,6 +14,7 @@ import ActionCode from 'material-ui/svg-icons/action/code'; import ActionHome from 'material-ui/svg-icons/action/home'; import ActionBugReport from 'material-ui/svg-icons/action/bug-report'; import ActionQuestionAnswer from 'material-ui/svg-icons/action/question-answer'; +import LogoutIcon from 'material-ui/svg-icons/action/power-settings-new'; import NavigationMenu from 'material-ui/svg-icons/navigation/menu'; @@ -22,6 +24,7 @@ import { EteSyncContext } from './EteSyncContext'; import { RouteResolver } from './routes'; import * as C from './Constants'; +import * as store from './store'; const logo = require('./images/logo.svg'); @@ -71,12 +74,17 @@ class App extends React.Component { drawerOpen: boolean, }; + props: { + credentials?: store.CredentialsData; + }; + constructor(props: any) { super(props); this.state = { drawerOpen: false }; this.toggleDrawer = this.toggleDrawer.bind(this); this.closeDrawer = this.closeDrawer.bind(this); + this.logout = this.logout.bind(this); } toggleDrawer() { @@ -87,6 +95,11 @@ class App extends React.Component { this.setState({drawerOpen: false}); } + logout() { + store.store.dispatch(store.logout()); + this.closeDrawer(); + } + render() { return ( @@ -115,6 +128,7 @@ class App extends React.Component { > } onClick={this.closeDrawer} /> + } onClick={this.logout} /> External Links } href={C.homePage} /> @@ -132,4 +146,12 @@ class App extends React.Component { } } -export default App; +const mapStateToProps = (state: store.StoreState) => { + return { + credentials: state.credentials, + }; +}; + +export default connect( + mapStateToProps +)(App); diff --git a/src/EteSyncContext.tsx b/src/EteSyncContext.tsx index 480d4b6..f71a29c 100644 --- a/src/EteSyncContext.tsx +++ b/src/EteSyncContext.tsx @@ -125,7 +125,7 @@ export class EteSyncContextInner extends React.Component { render() { if (((this.props.credentials.status === store.FetchStatus.Initial) && - (this.props.credentials.credentials === undefined)) || + (this.props.credentials.credentials === null)) || (this.props.credentials.status === store.FetchStatus.Failure)) { let advancedSettings = null; diff --git a/src/store.tsx b/src/store.tsx index 3c5b26a..5f5f9ea 100644 --- a/src/store.tsx +++ b/src/store.tsx @@ -27,8 +27,8 @@ export interface CredentialsData { export interface CredentialsType { status: FetchStatus; + credentials: CredentialsData | null; error?: Error; - credentials?: CredentialsData; } export interface StoreState { @@ -59,18 +59,34 @@ export function credentialsFailure(error: Error) { }; } -function credentials(state: CredentialsType = {status: FetchStatus.Initial}, action: any) { +export function logout() { + return { + type: Actions.FETCH_CREDENTIALS, + status: FetchStatus.Initial, + }; +} + +function credentials(state: CredentialsType = {status: FetchStatus.Initial, credentials: null}, + action: any): CredentialsType { switch (action.type) { case Actions.FETCH_CREDENTIALS: - if (action.status === FetchStatus.Success) { - return { - status: action.status, - credentials: action.credentials, - }; - } else { - return { - status: action.status, - }; + switch (action.status) { + case FetchStatus.Success: + return { + status: action.status, + credentials: action.credentials, + }; + case FetchStatus.Failure: + return { + status: action.status, + credentials: null, + error: action.error, + }; + default: + return { + status: action.status, + credentials: null, + }; } default: return state; @@ -81,10 +97,14 @@ function fetchCount(state: number = 0, action: any) { // FIXME: Make it automatic by action properties. switch (action.type) { case Actions.FETCH_CREDENTIALS: - if (action.status === FetchStatus.Request) { - return state + 1; - } else { - return state - 1; + switch (action.status) { + case FetchStatus.Request: + return state + 1; + case FetchStatus.Success: + case FetchStatus.Failure: + return state - 1; + default: + return state; } default: return state; From 37aaebbbd8d7f257057a6f2ffbe28b3226d638db Mon Sep 17 00:00:00 2001 From: Tom Hacohen Date: Tue, 5 Dec 2017 14:55:44 +0000 Subject: [PATCH 051/912] Clear login form username and password on submission. --- src/EteSyncContext.tsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/EteSyncContext.tsx b/src/EteSyncContext.tsx index f71a29c..f152cc1 100644 --- a/src/EteSyncContext.tsx +++ b/src/EteSyncContext.tsx @@ -116,6 +116,8 @@ export class EteSyncContextInner extends React.Component { return; } + this.setState({password: '', encryptionPassword: ''}); + store.store.dispatch(fetchCredentials(username, password, encryptionPassword, server)); } From fb660fc54c05ed1c20c2f9da16048812e6de1d07 Mon Sep 17 00:00:00 2001 From: Tom Hacohen Date: Tue, 5 Dec 2017 15:07:43 +0000 Subject: [PATCH 052/912] Make request types generic. --- src/EteSyncContext.tsx | 4 ++-- src/store.tsx | 22 ++++++++++++---------- 2 files changed, 14 insertions(+), 12 deletions(-) diff --git a/src/EteSyncContext.tsx b/src/EteSyncContext.tsx index f152cc1..a0ed1eb 100644 --- a/src/EteSyncContext.tsx +++ b/src/EteSyncContext.tsx @@ -127,7 +127,7 @@ export class EteSyncContextInner extends React.Component { render() { if (((this.props.credentials.status === store.FetchStatus.Initial) && - (this.props.credentials.credentials === null)) || + (this.props.credentials.value === null)) || (this.props.credentials.status === store.FetchStatus.Failure)) { let advancedSettings = null; @@ -222,7 +222,7 @@ export class EteSyncContextInner extends React.Component { return (
    Loading
    ); } - let context = this.props.credentials.credentials as store.CredentialsData; + let context = this.props.credentials.value as store.CredentialsData; return (
    diff --git a/src/store.tsx b/src/store.tsx index 5f5f9ea..011dbc0 100644 --- a/src/store.tsx +++ b/src/store.tsx @@ -19,17 +19,19 @@ export enum FetchStatus { Success = 'SUCCESS', } +export interface FetchType { + status: FetchStatus; + value: T | null; + error?: Error; +} + export interface CredentialsData { serviceApiUrl: string; credentials: EteSync.Credentials; encryptionKey: string; } -export interface CredentialsType { - status: FetchStatus; - credentials: CredentialsData | null; - error?: Error; -} +export type CredentialsType = FetchType; export interface StoreState { fetchCount: number; @@ -66,7 +68,7 @@ export function logout() { }; } -function credentials(state: CredentialsType = {status: FetchStatus.Initial, credentials: null}, +function credentials(state: CredentialsType = {status: FetchStatus.Initial, value: null}, action: any): CredentialsType { switch (action.type) { case Actions.FETCH_CREDENTIALS: @@ -74,18 +76,18 @@ function credentials(state: CredentialsType = {status: FetchStatus.Initial, cred case FetchStatus.Success: return { status: action.status, - credentials: action.credentials, + value: action.credentials, }; case FetchStatus.Failure: return { status: action.status, - credentials: null, + value: null, error: action.error, }; default: return { status: action.status, - credentials: null, + value: null, }; } default: @@ -114,7 +116,7 @@ function fetchCount(state: number = 0, action: any) { const credentialsPersistConfig = { key: 'credentials', storage: session, - whitelist: ['credentials'], + whitelist: ['value'], }; const reducers = combineReducers({ From c6ec2bcaf1ab7bd31a27d524a01074ae26c09508 Mon Sep 17 00:00:00 2001 From: Tom Hacohen Date: Tue, 5 Dec 2017 15:32:29 +0000 Subject: [PATCH 053/912] Fix wrong type in store. --- src/store.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/store.tsx b/src/store.tsx index 011dbc0..7583cb5 100644 --- a/src/store.tsx +++ b/src/store.tsx @@ -35,7 +35,7 @@ export type CredentialsType = FetchType; export interface StoreState { fetchCount: number; - credentials: CredentialsData; + credentials: CredentialsType; } export function credentialsSuccess(creds: CredentialsData) { From 57f89e38403874ec8f56875787a58baed598b366 Mon Sep 17 00:00:00 2001 From: Tom Hacohen Date: Tue, 5 Dec 2017 15:36:51 +0000 Subject: [PATCH 054/912] Store: make global fetch count tracking generic. --- src/store.tsx | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/src/store.tsx b/src/store.tsx index 7583cb5..7797a8e 100644 --- a/src/store.tsx +++ b/src/store.tsx @@ -96,18 +96,18 @@ function credentials(state: CredentialsType = {status: FetchStatus.Initial, valu } function fetchCount(state: number = 0, action: any) { - // FIXME: Make it automatic by action properties. + if ('status' in action) { + switch (action.status) { + case FetchStatus.Request: + return state + 1; + case FetchStatus.Success: + case FetchStatus.Failure: + return state - 1; + default: + return state; + } + } switch (action.type) { - case Actions.FETCH_CREDENTIALS: - switch (action.status) { - case FetchStatus.Request: - return state + 1; - case FetchStatus.Success: - case FetchStatus.Failure: - return state - 1; - default: - return state; - } default: return state; } From e75d586c0f414fcece33231e47fece0a96f64ea0 Mon Sep 17 00:00:00 2001 From: Tom Hacohen Date: Tue, 5 Dec 2017 16:13:47 +0000 Subject: [PATCH 055/912] Move journal fetching to redux. --- src/JournalList.tsx | 70 +++++++++++++++++++++++++++++++-------------- src/store.tsx | 60 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 109 insertions(+), 21 deletions(-) diff --git a/src/JournalList.tsx b/src/JournalList.tsx index fd982f1..8beaca8 100644 --- a/src/JournalList.tsx +++ b/src/JournalList.tsx @@ -1,4 +1,6 @@ import * as React from 'react'; +import { connect } from 'react-redux'; +import { withRouter } from 'react-router'; import { Link } from 'react-router-dom'; import { List, ListItem } from 'material-ui/List'; @@ -8,40 +10,56 @@ import { EteSyncContextType } from './EteSyncContext'; import * as EteSync from './api/EteSync'; import { routeResolver } from './App'; +import * as store from './store'; -export class JournalList extends React.Component { - props: { - etesync: EteSyncContextType - }; +interface PropsType { + etesync: EteSyncContextType; +} + +interface PropsTypeInner extends PropsType { + journals: store.JournalsType; +} + +function fetchJournals(etesync: EteSyncContextType) { + const credentials = etesync.credentials; + const apiBase = etesync.serviceApiUrl; + + return (dispatch: any) => { + dispatch(store.journalsRequest()); - state: { - journals: Array, + let journalManager = new EteSync.JournalManager(credentials, apiBase); + journalManager.list().then( + (journals) => { + dispatch(store.journalsSuccess(journals)); + }, + (error) => { + dispatch(store.journalsFailure(error)); + } + ); }; +} + +class JournalListInner extends React.Component { + props: PropsTypeInner; constructor(props: any) { super(props); - this.state = { - journals: [], - }; } componentDidMount() { - const credentials = this.props.etesync.credentials; - const apiBase = this.props.etesync.serviceApiUrl; - - let journalManager = new EteSync.JournalManager(credentials, apiBase); - journalManager.list().then((journals) => { - journals = journals.filter((x) => ( - // Skip shared journals for now. - !x.key - )); - this.setState({ journals }); - }); + store.store.dispatch(fetchJournals(this.props.etesync)); } render() { + if (this.props.journals.value === null) { + return (
    ); + } + const derived = this.props.etesync.encryptionKey; - const journalMap = this.state.journals.reduce( + const journalMap = this.props.journals.value.filter((x) => ( + // Skip shared journals for now. + !x.key + )).reduce( (ret, journal) => { let cryptoManager = new EteSync.CryptoManager(derived, journal.uid, journal.version); let info = journal.getInfo(cryptoManager); @@ -90,3 +108,13 @@ export class JournalList extends React.Component { ); } } + +const mapStateToProps = (state: store.StoreState, props: PropsType) => { + return { + journals: state.cache.journals, + }; +}; + +export const JournalList = withRouter(connect( + mapStateToProps +)(JournalListInner)); diff --git a/src/store.tsx b/src/store.tsx index 7797a8e..77e0046 100644 --- a/src/store.tsx +++ b/src/store.tsx @@ -10,6 +10,7 @@ const loggerMiddleware = createLogger(); enum Actions { FETCH_CREDENTIALS = 'FETCH_CREDENTIALS', + FETCH_JOURNALS = 'FETCH_JOURNALS', } export enum FetchStatus { @@ -33,9 +34,16 @@ export interface CredentialsData { export type CredentialsType = FetchType; +export type JournalsData = Array; + +export type JournalsType = FetchType; + export interface StoreState { fetchCount: number; credentials: CredentialsType; + cache: { + journals: JournalsType; + }; } export function credentialsSuccess(creds: CredentialsData) { @@ -61,6 +69,29 @@ export function credentialsFailure(error: Error) { }; } +export function journalsSuccess(value: JournalsData) { + return { + type: Actions.FETCH_JOURNALS, + status: FetchStatus.Success, + journals: value, + }; +} + +export function journalsRequest() { + return { + type: Actions.FETCH_JOURNALS, + status: FetchStatus.Request, + }; +} + +export function journalsFailure(error: Error) { + return { + type: Actions.FETCH_JOURNALS, + status: FetchStatus.Failure, + error + }; +} + export function logout() { return { type: Actions.FETCH_CREDENTIALS, @@ -95,6 +126,32 @@ function credentials(state: CredentialsType = {status: FetchStatus.Initial, valu } } +function journals(state: JournalsType = {status: FetchStatus.Initial, value: null}, action: any) { + switch (action.type) { + case Actions.FETCH_JOURNALS: + switch (action.status) { + case FetchStatus.Success: + return { + status: action.status, + value: action.journals, + }; + case FetchStatus.Failure: + return { + status: action.status, + value: null, + error: action.error, + }; + default: + return { + status: action.status, + value: null, + }; + } + default: + return state; + } +} + function fetchCount(state: number = 0, action: any) { if ('status' in action) { switch (action.status) { @@ -122,6 +179,9 @@ const credentialsPersistConfig = { const reducers = combineReducers({ fetchCount, credentials: persistReducer(credentialsPersistConfig, credentials), + cache: combineReducers({ + journals + }) }); export const store = createStore( From 0afa0e792e339dd1ab073020e60d98811a0c5117 Mon Sep 17 00:00:00 2001 From: Tom Hacohen Date: Tue, 5 Dec 2017 16:57:10 +0000 Subject: [PATCH 056/912] Update app name, title, and icon. --- public/favicon.ico | Bin 3870 -> 16658 bytes public/index.html | 2 +- public/manifest.json | 4 ++-- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/public/favicon.ico b/public/favicon.ico index a11777cc471a4344702741ab1c8a588998b1311a..ac14bd267619c266b643ed3652c546962e63c2dd 100644 GIT binary patch literal 16658 zcmXtA19T-#u)fL0wry-|>&DsG**F{9wrxAvU}M|f*fuw|bMx}QcitJJW~RETtGmCd z8UO(7YwGO0xExPd;eQtpuX118`jW(ubZWT zBC77$7ddX7CgK@SJ!&rUVP4e80}wcYG-yCdT9UwY%o$ycz6af9f|VUBZC%|o1L45R zw?D1zrjXx6ehy?}LZ~VRP5{1E2yNGl)^0*e@wk*h=w z^e(&#D1f}6eh)iw14G9efks>bFlQ)_{&fwv`6u5=A)H|DSC^C~e+qPe`e5zAlY%fU z__PBw#leyiWQ^c^2#n8r%ukEin2;VI7Z$PBLrVXAKwOxh-`|2gPufBB;+^;+BvdZ%mk-Mj&KtY*>}LVgMpVlZ1p_8g zhTC9lzJ`6lC%Q!_|9NhB|{zws-BFbcaO=r%_!>A?;a$y|5 zi0~TL{{N8R9s*?`$AzO}GS}05hTyNp@0DYgb%^*p?yagxBEFo#{_mV#r+t}X#+Xh6 zRTUE?O{L09k+zeAAGw(6(qIR6GA+l{%e=k}4U#_lVq3Efljr5ov?oIQnl48P&=ES- zpD|lU>0*pO$)*N7?cM$!M@Je_K`4%Il!*_^+daho4j;S15BjWjTBu*Od|U`ho)Ee< zcB9K43F z;4VF~uel9ojw&2*wLLvd{oW7q+{6-IJC%xp!ZWIUkPKgR@5 z7xsPgeDlLb;=38!W9-f2;VMHaaX70Owf*B}CM3(Q?|p8xbun1GtAs-)7#n?)~PV(;H(X3Dnz!3_E{#`+DdLaMAM)S z=A@!>FC&fa<=XeQnumT~8;0M{LZ=M%Ad8QUG)V0ObU-!KVX>k*Pa>ZWzkZPm5bazL zu@Hr>C7cQ%P+2Xq?q$gVhW%MGYitGcm~2|Z>m%*LV@X&^Oy2LCplBOP^~R;QtPD^( zB%>J!zWc{l#!19<)?>78~E{cN_+JJB^1V+gxi6 zc8Qt`;|dRP4O7JFQ`C4>z#uSl%LSx|r9Njy)29+$zBL!(3kow(uY7yot!p)~#-1uL z9+h;9mVJi_l**srUgigZbvGUM!*cP%7zY5`+m}})Nzjm19IrjzdiFO?me8a<(51k) zPPjq2@3qt`?^IOvHV*?ZjYw^#XD{A1z61!<5eNB@72$)`T34tsuhVU&>hp#($@xA~qyx4FIn8%D%Y zR&&W*Lv{U(;yPF+#V80y!$dklZC?SGERZDM2)SIn&`PXd$i*;1zY6We+jw0(Y0H3= z#MuWhf;{?ZYZnX#uA2g#+M`R@b8Cwn+_ss?4z=n*U*l6I?we!vGN9~!kQlwVHb7*$?JgH0DB|g3<*GQn^T$qGP<9_<8&S0-#&~Ix0%Kf9k<&MAG8xD zWeG4=z8KHd1x~t%SVZ`1W>m(90j+t`_IS7Bhk?R z%3<_%`x8L7GX`dyfEk1?Y{B7Kyi~0^ZN^5?aCk`C_{LAFG=tZUEKR*>A2+5M3_ml^ z8;)u*1WHWiS~cfPyYMlZmvclU)+u!8KXHp51n!N!z$p3HdbK9;7nupogQqu{Pn^&G zY`6fg-f}gZXAq(oV?u?wK+4A@V?ZGzb-m5>4W)ZCXF?ZS{xZRc>~pu01xJ}x*w>pm z#Fi270GN2x@G`U>OB7Tb(pef&3T28hhq1C+dWf`lWjXd3Pos1^0kDIavemoED&k!) zJ|T7n>c8Kr7xjy|iIihQwb()2QBheu`JJgLzu&ZInyDzT%~4DLwsr;3hmTTCEHWo~ zik8LY0tgl;d+NZ$Cv)v=K47+21!Gc$0>Eu+I%#7*O8>==z47P`?mAr_jNaODyIxLf zo(f3i;T0+%l8OhTkgcMTrnk0`tmaXIwlaULA>BhLe&a+lE@D%{I6e=)VD@s9a#Jye zlgh`efRaDB)TMS@EP1M}W&VpE>2^~L5^_}CW+0gLqnGWB33eyJ5FI?GClt|ixPK2` zJAow;nal=J{N{;<$i>}1ts*`Q&G}qf+;yVdLp1pQ-O0`!?g=TcKkiWHpT;)=uB+ot z$1zHRlWkcFZxl32wmpSWrppWowe)fa|J4YBm#Pb5aZ>&5cct!zF0X?cXxXDk#yU35 zzthbhgf%^O9G~4FiAzFXEOHTIL!6;+)=F5q^+$_(g1Iy7o&x^g#-XeBS2~uMv0i^R z9D>`fyaY672wViCDPmoZ=$1Mi_tto zO~2uJg4@SG0-(tF3vc`GFIoay2yRJ_yw;z>-wD8g!}|pZr8jZNpxkLY5W%-y@cx-W zt`&QFbXN@d~%xa!{52#boJzMaB>y9qqwTwx$YKIxm^sA{Q-Z`KxSv_ z&pYHTj$a)XCC_Z;zrhAYKdbF(Ya$Htu!R@DarM3rus9(PjG{?cMF-r}js^@ZcG~u| z)Ct?Hl6!)~>-UD%4Lna6>+j8tRGabNhg;jlyCwd3XlDA4-g*OHQC!oVJvmhsxt!{X zj6v(X>agahGs?HlCJrqCAi95-l~c3R|IlMJ|19HNyzenCZ^-EN<4xIa2Hk&FQ4;{4 z2OjdGiB~LN6HZR0>@QqD*Avtb`8h$rS$Y^<`t6qlYOpy83QA=nm<6!*d+cI2*v;VIPnPcI{yqcN7(v_#$UxCQZT%UMxZ{Dg z%?8mu;=9Y6TeICa{a}V`*pn(g!_;hWVD8Ni8|49m`+?xh_S}y zgg*}sCSniuX^h%nGp*wJSLINt@8aLjuU`ShOK7M@|5ek@w zDV=Cmy(*|4(Rr%GC&|p#iHlfF+94Z`1 z?Dx#I0!0Wma~`(hpq452;I-|AP=R_d5fK~FS>ZXk#e;>O~ zs`G8fxKZDQ{-U@Mt!ldirCiyNHjX?!h)(yu(i=F+NOAF5Yz(-%@)S}L{gkbY8fA^5kk9 zeUXl!`T(ek08m>dV}rpQMT}@#f<}#xR-;*4{vIJ9X16`F(ewaWzB+<{JuH3Gs!q8% zR^@(bGJ8Q;T7qLjghaG;EtD$L9CwzI2t#6016g|-dU&^n2|pClU#aLRXJ93KocS0& zOnl;92A`07EkfNt?0&43y?gc%its_kbNS32DnhzLy|?5tr0HHmU1f>)JYcTe)w)_N z%26X4D`nJJwXL0iDRoHg7eedaE{#gGUJ3LV^Z= zR4z~d+F%J2{bt70jiyGUbWaJXu;&rmMlKCK3KX@bB|tscf)9JIj^Ipdnur2T+jWvXlr+aN>>;nq@2|@Gii*Sz z9mS80ChARKkd-}jbi&jNX&TAA9>}Yg@wroYBc%)yz#(>8kEPJmgxmcfPRS6&&0xDF z|EWUlv}CiVrx#oYM+raS15>q1BX6570KTHglWhFA#~`8wYZvavLbv36K;neoZp3Mg z8*&D|uU{UnOt^Ko87Xov7&OQXVSP1-lA(KSwGNaP<} z-ikd#TwIMDwVx~)A3c1l)f+8A!_L#h4ODr1n9D@={az+VShnIH6mU{V_%1|(gYBWU zYpv*$HuIgDR9~!rX-2oy0{$Ex#nDg4&F!ppSCAQP53URJQQ8bmY>qR~5-i0#p*W zao*j!WElLOcusk@(Zl5SH8~6k_4tf%EtzMz{cw1rteU%th1{%){dN1u)UWt+ldwK9 z6rlWdbgSA!riEsXfIHEYD_AK!0pL*k9Qt~P^XuyLk+SnBkQ+{?;nM08z}#v z#wJruOd5|F>zyjvi4tl@%&{aYJi>b|&VYq?~ z2;NOYcr_Y1Uud zwG{G2Jh_8+7;kA@4lCJ<=lWZ*$K@;U4*fYx@8xTDDbh#xP^r)1?Z-KsSi@%J-7ycZ z?4g;XdQ~UBNR00Rzd-}B`??qKjpL-v@qr+=ST!>ukJ{G5*DPLRq#u%ihY6yq&Er3Z zurJ6KhMy|^2il*lmJOsZkV3@Vac?8JY==4V;820${Izf8{7$nqwC*Vse?+~2JL!zJ z>B?i={p2ar_1%*+)3Q*Vdx@bkV++mGSM!@KZ!P!yj%4;PJE6*C@Tb<0ebvc4g4-hht~j!gWkXW3nP8`56+a`uJ+x!Ocg@D z0Qv-uqChY!W=uapF&dFrcK+gPyb{c**-@MJkLvHv{RLhE51 zktgbz`*IWDDk^i7=aB7r=x)&MHBQGixNxMn|7N?_lGOY@XgaNw*7@UOCTX5{w+hk? ztc?zxMoa6LRc)dqor2YKz-wW@G`z3>9J%zfD#Zx%zcW?)2u*so(I*=$i|ixCd(5Fg znVu+eq11aae4F3xAvJE+%ldT!Vahkp0f<#=-lUYB0BRHRrJrSWtv7XY@-Juz_ks10UAZPO_l zE`QG*3={hp`2bkr`9=Oh2zl!Z-_nn54oAE3VaXEs715Kn4u*G1SIqPvQp0m2_g^jO zTzIQkxAu63S+st!UTJbyS&)M_j=1@N(_T1F0bz*7igSVC-Mvqk3ip1K1UK#jPvbbP zJ_m~^kPd$u4MknrX)FB>BZ(vy#c}E9xsG zUDI6Ur#{!YR_*pAuz15JL~<5k&6<-pVNU;?C@P|KjY+A0r+W1#^ca~2%q_YL&*m6O zvJEZDSq~<6FF&qp==1PRpF9m;#m09&5T*pJH#$oI>(X1*_~+e7zRO_A4y-jCl6kAF z3V5zwcdX1oQIpL_(!Q|l(l0#-2P-)|(0%sg8c1eMPCk2ddf`an!>J6*z&bKv((JmP zNb2=h>`jfu3Tkrie#*xx4nJ-YIhirlJ-}glaju*z+BH)w>E@SuhJd1tU3*1KKKUiC zK*g?V&1i4JP@b|0OiX(G7ca*ezmRwe`nmh}8=t4Bk^*Y959}nMn+S|8yuec)AS{;3 zsZZ`CJqq8Y2fF;J4Hlm>>Hr&L9nRAjfsYwt4-m{&6S@3<(>_)?7PSL8mkDFKe#OH62qBuWl`S1ebIdA zp3x35x#vC&2Tb!Y?EMZY<1>Y*bmfZI(T~;*kro+a;=88GR*jWX?j@+e@xkzx6A@F2 z@Hzcn2XV%`-bip4G00@utu!#~3SsIKshc?8II{gntWiwo zXI`_-qBmM1wS56)rg7pf#lr@PSmu)MBn1}U!}DYhFyb7k3m+I|3l10@QtLzu_zV{d zG?v}a^}gX34(HqM+2WSE18+z_0(5sH-Rr%Lpbn`g-SDe2#aN$)Dp|a!f~>$T zY;eL;(K*sLVVL!82h$9{hH8`tX;kpkR6Q4iqa~YmrH{Hk1YCS-CdLp`FwROL-xcxk zw#Ny{hIz1c?jOJ!s&+5Vkt>1=viH<<7M$#)$32Okvm9)qjejvM30| zIv&lK@(9b%Gs6>^cNpYYLr57a@0ul@8s@GAS~Fu6-{8*4{>k3f7EOm$T3`-4_rA|0 zv-bjM03uh;m9Bi6v|azLn@*kAl{r>dqJ(#B8w%u=iC}A@&DFRLovL$!a>GSNCkQOq zi`k<{vu(fQ8>U7WBPgRDQ%Uy`G<_3anelB(h?a2LgFzYVEjR-5WM>awA&7+&T?lV=u-zqn-mBQG9$QTVU4rMs#S(i|Wg_{0r!*WVm)>kxG+}l5(eCMSk&wW5hfFpm64$X0-=tX;a_ZhzzK%^1f_e0+*N2 z>l7qtmOHy-?KC4FCL>+oHmF8zT+gGu^KaH}DzRmjh&n_U6o!E)thT%O96bh}=77WGsB6bx6uKZxmdS2A2mpAKcSpFfA8g5-Z zAJEwXoUxH@(tf8ExU?`{(0kVWVJC8r(k3_l=vCpLlsaDk_@-PNGD0pwf<*@M?#js2 z_di{~Uq%q45@XybuJ+jxBB;g8Qr9*(|fgpT6z z-Q}J>tL3-7Qc(rXF>pV=sa5sd@z^*x-*M?iKFhB>`(=OW156Y44e@A*_*k~D$?Z&5 zr-8A`NcoN2sNgg=1TeE8Et>+l7;5=WiG&scbS8*Z-L*5mdp0ucVQxC*Nfr&Z`wCdW zuT(0Td-hEv5~J`y9=o2`llF{)-AB6VIOqac%sjKJROzAl=WvOLzOVFW5)LXbb50Fl z`%-#uS_xqZlI`&}s%1}01{4zlTMtgn$079*x7pMU=Ml1>4$4k35fV)yNMPlN>$;F9 zLT#&MQQEj{mn_X?(y5LlJxn3?UlAI@@3D+AIsFc-cD)3B)$X~+3>Z~_0q1VY{_W?d z<`*4;gq?0r%xAoMQe9dUld;U9g#^~=!+1mK8A+T`=n|&uzJ(2lt1hM6FnwhiVYbXH z+5kwN=rT)?87c)rY`Wz3+v-jiwmzo7ch(Lep=+I_rD~Nczsonzk8L0J=cfP>qMWL8 z8rBti2r>LP!!Gw`y}!|hKqg?;a3Cz&kBETts8we+NCbq;5xuv%YcY+j{N-=me56xSbJM+8u!cq(M(@h20w7g<-w;HYmmS z88Holc(X1Gkn*Fxk&yL=^OVn10X?_sG8GTK(Qr0kQL^C>KEI-*zd{ z1v_M2*da?Rde5duuif{h3@YF8Bgse zEcbqM&;+jP>k+o~d^8@YfP~agg^x|=Sk)Dy18Znz4~rEpc|B-u{PwLGLcwGsdA_*4 z^&xKCgyCST$luZ@>scs?5>wqdqpA-b&o^7J=zDyeKgt-6=zCZE;`^0M4T*ty@P*w6 z@On)DUCEB*=z<&9t_sC6GCvj8LF zMDMUxFG9VBxJe3S(@ORiLduQF4_peBT2Ivg2EHisI0dg zWVKa(Ql`6_R%W6&3tufYlYuyMDqk-RGpH-;VZd)2(n;zI63AMl5r&ETb?SQ*Rla=G zG}Bwfrys&@3~a`h5)~P?A%6r{Jv_xp(yL)N-srr`4rN*dL=94_T|+h_Sasuemelf)gQJ5qNS zC8z5f{MVMlVocTcrgGZ&>{e_~NNiFtbIOVTDtT>i;G;I$&o)AJK4G{s?F^Dt)`oIq zMZzZY*G%Im{nltyvfi6g^{N~LFDk5%IOqazY}3W!H|_t03rh=o@QZs_m4|w1)F!KS zD3H%7v34m?tL@b@a;DBMukUm=ALX$Vu zqE_5KA4~F!j)<-KY8HRi@S$xa!tmN4we4~wU?i!LRk+qFRvY8SNCwfBj9z?WDnWad z9@_uTZY~yb0c@gWeOc-@Hl-xqw_+Wm%cS&exjR(Ft1L0}$9#`ya};Z4cH{l6x^ND* z7CJVWn>vlLM^`^B1vG>G=-;wlbt5HBV)f0!qv8_p8cV;miFu4j1T^yAc?uD(! z+#QwM#$bPCH@p2BDmW6mY7}lloymLlZ_p(A3S}!sW>ZjDeQj%!JJ^+FV})`yWp~vH zK`<10jDz{^(}vK!b?C!8EwOt7seB4{-Jp(S`Il=UeV&y02-7FRo* zJ-{rB-wvQPaQcPo)VWE(@+2R9y|>0g#@1wAMPmNZ*KVkWZ>eS(-1E8kK+v;o^PSE{ zEGxp**mp{_Xk>c2sF_l(5T<_=C3xp7hv4)h)5Tc9Orz0nbL|h5Cf(((^NSru6wy#% zzNUX6ahm46RlMICxYR^0moDDVgmeM0#eX&PVh zyqP>KI7WCNURHoa5*x-K6uVTFabV>z^7{!lsP$IuSM3yf+h>co9j9!*)0{`c|NUE; zYsPC=Ii}ak;dXxf9nDx#^?AbQb5wUdcSu^+ws0HYG4VD#>X&C%KtX$~av7o_M2c|X zy)6d58%N=UA5M~QCufa$euNr)v(n`eYBJpKLnC(6&%#`mUC zyB1H=>Ewg3wL7q%3vdZbx3HCXn+dJW5@CP-;8WzKOcV;=SkjOrPqXatosSIsSm5^W zvj^K=zvq;o6Yrlvk#GQx@(!!PnlO-HR>r1bM;1QIf>{%;$GN> zS|s{Knk$eW)L7je&zh5G5FeK{sU*E+`7kY4a&`Qt4|Xj)CX@M%inb65SgPz3@K=X| zFKZ=B{+`z7C~!v6#z!AW3~hzgc>d%(gW=;~9kQ%{zPB^&XBJiHg zTmIN=H|(f)h7;$0F>mXH^M;gM5_D7xhG`ns6PioJLKrwxZ0HF6Iu_nouvGQUmb561 zy54QzmjYu-U3>;|)ji~;zRahu&U4<1IaL7VB_Z^5CP|eDDa$!S5DKs`ibg!L_g5*A ztXlUIz%Pk;13WD06*3#(V)A$fA{ZDmQNRX+n~cIou;rB1H%OdAYJ}ch-%YHIDNgs} z*5wa_#kuBcpo~YjBfNh*3`2>>e+ps+NL^_aHxNmmxfd!1h#;kM)${Pik~U!YlMlM_ zi1_5z*U(~$TZ6bCdlFS2z!~841xyuG4#Jkwq?@j?PGJ*IW-`@_#Lx>x#uAjFnQ%`c z*iqCJcjbvv{WeWDNi@6OFpwCxM-t~G%=X-^$ZE%2mQZ7s+1Y}XzWdu%M?o6I37|dR zoQ+KHC4czJ^t9j`7mHOM0Z~2~VvK)-LR^&vqo*XgPy-7Ic2TadmY_NSy>MUd;7z$8!Gg%G{hVAHsq>cSU2ny_)FbocAYS~)ziLm%mA=`fSKn8i z@3zPkvsjkrC+8;55Vq>uxb`JF=5CH6&vFzZ!>Di*N*Z{*G_?4~5`7&JH`fXCQ-vl* z-9iilZ)V^7ZT3zXOjs^A^6}-O`PgZ2qeD}3MbxDU%5BEtc2srp+QE^!-s|{MgXFhK zELU%m442AM7SGljoG)qYdoj3_wx5D&kRgMlpHYL^e3o&;F{zPbN5%*txlkh&!2Bmf zlka~Oec%fgub2|lBAj0iM4SBuMzc3EuSM(cuOs&xeD; z-Q&;Hup#oEx3^1871V}V2q}ab8T}9LE4H}Ez~Af+)+SA@k4et-?h!l^6WP_*JCTeK z2a$?&8z*B?aR}?Vp9PT1dhGsSqYP52`gVL!erKpO_VCaZ=seBMpRSNV-+r%yD3TB~}z&dbFWKgxioI_rMab7(jJ*1(Fa zZVtI2nczKMBVP8s#*}3pJ*Vs_P3{FWLi5@giFTW$t!+)sB)?2*m(+$&>jBCkSzDPx=Z59!y*%4#5y$RV@j>G%Y^ZPF7B6jV zHOCW|Py?QNRU$~3f*lMhPtcnj@8-fXJ6mXHb1`UNs&cfi^G__~V?im9M|~w%$pEsV zXV5U$wcV3<24c{7cz-;J&16GuiEhL9@w2i1Jd4q#ObcG3gd8~K-YEkXjmoyb7_o7f zej40T<~B_2(w-%)h%-G9l^jt}_)o5M~C2W61N^{$AiQ_Yi8>#~wcxSj&T)3H2cVIsxOI*N4C{-~xT^g41Gh0;cW9 z-K;OJci+c1w~vk{&6wHsND#>T2a6-1TjXpDeXvZ~Eh=(?36r+Dz`@QdzaM>j`Z-y$ z``bccrVQpb!qWA^pu@NJ3LXwpx{nP*lDK%sDw1>KA1s`!npX;fKjncPG=I&X1ZQZq zZ)RsOF9ibN{^jC3^7RVG7M_v9a{FW(7(Yq(&IjGq?kllrJjqx>Fz&iyvUwiKB2Dvu z2f-Qq#BO^BMYX+e$-Z11nW2YZHX`jgTaHY%)Pd-ZH(qIY(M5{l$sS=DwLip(He7(? zWMz6of;wyy*rXx5&@`j|g-o<%>CYDZSus!WBku(+chqfp!%66s5NXeZRxb=|8BPBI z)W6$HV3IMED*mB*==TcZ9HxslpE16uLkL2Lue-Y*c7`qwWZM7CFEe=Ph4nrSh`a9Q z>lwDmQV<0W01bsv67S)7b3DtObPlX1h2d5(zaK?H3-%2c9BLu6BciUbqjB8A>#2{E!5vjEMoh2*E2~kqo)eD(=GZVcAPnVj|pwj z(Oy6$AWC{;N;A5x(H?b;5*y@L!~W%${DLZ~jrOlu3#oj-H6Z>0Q+Hj;40P~TrSE(a zpDea|RG!S%4iw_;dnvoJCRgp%&BjOUj;GLY{tOgWqx^|Prwz!R`e=Z46s1VYN`gMb zWzHQvw?R}Q%|I$cTze+q0)1W&hd$f0RLHM8!S8%@PIhriuhjCUbHjIWAx!=gthvFYyG!veHdG8 z_<{=Yev^!3TwU$VfZjCKpW&lAUG6g&M(wE!_nN+UE)Zzn!7_5cGE}B_;G&^b96(pY z+@j#De2|!WV@qjqNG18vHNk9iqK^K7nmY`aeaACfByyGkFe^cL=uAl|{aS89*d-W} zppFJB+SAK7kg6wG~dj?TROS<6sq@cq>IYbMbgUE3Y_ zGmo734Kvs)D^qA`>aUSt?WrZB;L#Pg!R4^#jcXi-XQ2Ccra;{%kzZW>8DqIuw3b8- zKL%UEoVgXF$p2lfd8$I^Fj{NMj{SSG>CO$VfFSnAZm1+-nK%7Zf_+*mUkr*s43yF3M{~D>t zC|Y;YpeZl`FFC!JenSt#d!5f0u|l9d=vUd6jVLx=A^GQ`7!SkLUf{f)xUdfPpe95f zr5lRsws4@>ZT4cWS?y%-!$r*5LQ^$iuqml8M^x_Jk>wKh@9$?H{ephll&9OoK5P}v zEIxbJ95HJohCTgHXAb<(G*odz{;VHCNc6Q%90*lK=!beGL5vZVjHar93kkKBm@G0w z=n*5-WCEwM8j2HH;7nRNFzi-k3Xea5d{`)k!rWmn|1F-Z`pIeb57TvTuM*HuioFrF z%Lmib8(U&DmYvs&^^p!t*ax*N8zxdZS9aqY%xhF&H`+aBf$F{1iQQ4g{?-2Zrh>7! zJNYb8KY-(3n%`Bc(Gd9Q(fg2yAjnJG5)MsAA4v{8FfN zP38dMF&Lp|HYP&;Aj$VMC$sHCgt4HkOdV$QcpUkQH@m;4!_&bh8qVC1cb(X|i(_ci&_i{B9MCjlqMl+n*rB~w=i zI)L7s8!c0om#^}z?qF39lO z#4;LU-2S9~d`+HL<50=8+G=~Gr?l7XW8#)@U^3NY zC_-YTvQV%n4UDK5WiW|nUSxTn2e4{vr67WOb_{kzN6R zJRti~MU!8^3ZPR+h}4jLk`6#-I?1XfQH?V7iX##C?%9bXhI)-egj=~Lfq^i)3Q^kF z{=N3zhswDnLQ%&fcP=(`PTEy{k!uniYaU`x;O8pf{+6gXNimq=2Yfa;hJvogYjPb+eh`5;8K_c3 zhXrTOIhYw$90jfuCNSTngBrvBcC^FY(wTC|D0>-WIsi+06^*Z)vfNg;2>K*Lh~-fl zOj9)j7|$6eW4gW05-0`loSdvDt)5g@++5|m@poibvco(q=MA4KF9>BgC_TICAP^p5 zBv!AVWi~}xFUh|CXA&7eFB_1q-`KC`gpB)W@uk_&TJeb4ut^BT|5lR26ZJ%7MT!w~ z(WHlesb_2OWpP9}V`x+&iIqXrNpo=KG9-URWrK~x`evgC!6&lEey{3C{;r+y2+&h* zR=hv|9IKs%?=4*F%|N+fA8#yWzOim-ozr0*OBSRrlkv5=5hrW1j5eFLv~4fhAN_X4!*!Ak;Fd!oQUWP!H6o1e76|C z(r9^CGgLg*_B_Z7&jpkSuh9!H4mzycXPGenC{aOf=)|uQ$>8QSq3+Sz?tR*K@E?Of zfpcFP<>Tgk{poYdSJCt=|IDUe*Cq-2itNr8X@$r{`2uf}Hnz=CvxIK!#5*PYZ3-YA zUP8YH&jClzbx|HN_xjR5ieA5v4XxFFqL(Z&1pq_u1E#eeDh0nhj15VaZSh5MHus`7 zC6J@O8JPf_TAlG|@I(}Dh`U{d@qTn0cA^pRc*0(l8T*Sd6DaCfAOe>(#ivMzB}`P{ zKR>eFr!wiciw!BR&#l7kWU4@Tdu51p742w11!3S=<3;#$Ppe1d?=!M!y0eM;Aq2@x zHi6!R!=977wBcG{9?$Ps-kP4Y z7$T)l5v4JRn*09x^MPPXAmJSrdIL3~+R>=Q(_a(dT#U3>Da>1Y6wM6(r$SQ)odz)V z2+G(WJgyVBf+u>OTKj2R)}YroLgc0pSg8+JM}LwEu*4Lc6Ey_-W>j@SyhwA4nn+tH zQ(3_2eUHS(7V4)39Vhu^eAh0J`a#mc{?FuK8=X*(6h(Bg5_*k;#aRqM^|Yl|iv%7x z>%d7rRK^0+VR)Wjf%-z|yl?HJkMZ{RzNNwM@83q4CZ+nd= zh8THN#F6L{Zb`>UQBPBh7psfnzzfJ>7$ov^vT6nQYplv}is;pE_RJy=x7+fsr|*txTT^uM zu<{E%DZTj2^3gI?5iI26%k&Ng(pJASEt0*sA!iYOeaH1b;*Tk7Ir$OFl#6{{O7IRp zHwO&2zQNgzRK`53ia(ioyb7XGx6uk z1M=%s7|Ra?dZEpLQ_wpqjIxf#6MkSsk!i?%qM>GY$nwdX!%%I7Pn%2d}e z#&wm{4Giw&3-}LFx0?mZf*g+RUR?TkU1K*`zP2gUJ0!YXm ztJIfl9vEJ#40wNmTHl<1u+sy)^qSq2nY>cH`_n!b{YLg631;M+%W`3?Pa8kvg|&0V z6GP-kQ3IToT*jF8Q$HEfAU|I=w@-Rdl13T1FBo}q^b=_WHiv5}4O_eXA4Dt}@~m`x zy=-R;@1WbM{j~)GWZmgumJKh>@53w|meBlNmSo4&-KXjthg^ssKY!hLBbnbPw3fuv z2YRYyR@q!Fu~x}crB9r%P7Z725dQ>K@I6SdN;WM0IctKLnKh4h`o2uyh&?pU6t$Kz zyK4&!p+*n5K>V3B0uu1^&A|wy2iE|Ivw|i2?~N;@K>O`)zduc1nDzV%C;9a8WJS8E z5L7_Dz{N9sI};-M>G|=)lL7bTA{AS>Dx`EhM*GIw4Uu`7!ZBo)<5tWKVuwglQ3u8& zsZ{-JTUy1uUMdA~H8oM=;a26)P~WrXv)Xi`fPZz9*#cy8s^(Y`v2wopyfhy4*$JZh zKw;^V91FJ_h}w*;T|o1fz3jxXi~+xz{bu+OyN~wssCyhff2rqcuT$)gS{8$p8(6#yhg+% zfxq7H1s%1Aar@0O9(N?4ivK}d_W_cVA7;75vGiCcRwqyg=Wpe{^Wm#^C9*;EzW01x zxt6yb0{-}>&Oh=yUlO>)c|E4@s=T}2HXNK)09cpCP1sCj+lWiA-RnA_cSI6m%`Z2E zj@hNp|3Z?icbOh09)(;f6I5Rs@gogN^AwCBlhi*YLDjy7=S??bB!UDBh2%Fu*8%~d7r{zABM@{a;cwxqm6bybv2h)9T(I=j-%&Qph#y_a2n$A-MmK*2)5_+WXA(&AoBlGF1 z3NOBrekXE?UaY>7ch}NWVb)BM9wu^Qi>Uoy%~ii3P9YyBbIXR2qnW2sA;d2-9z0Z$ zuI(0Eq#rhqUo9!s*C~z?%D|Gzg7V3)$E<(#0q_Q#f%!z5cA=p7bg{#+iGnY!SYY#N z5Bs__;tV8NvgIU47BpR2;0R-FIIbuDkQb8S_;>O4MID_B$Q)<{)|z&$HXSK7yh2i! z6^&Q(P#7NDa&{ix?%bZ)E+C^Obx493zrOcq_74mK$#NxIkei_SrQnydrSZ$z0<*#~ z8mgg>;e8I>Qeqm1zjvO{6ZF-y;9#c*hyRuy#+6ynlr{M$jbBB8908Z-N+>YS_v(m! zEh%qO%}zoi?%k%>T1t3bWp_$@e41ulp`uYI!n(TdrBtJAAO-l|b5;nQ=x0Y=6US2{ zaO;L5049eV6WI&b*WJQ4^;0vj-PW^|NR%hj?L6FBwc{emN%FR zOQbtAI}6^ff^_ni+m^_wDPFPZ?#Pchfgn#}-C3)#=$P4#O@< z5Yku5`6C$<_*)J=U~=%5>C)~Lwdd>f|7?Lc>nk2LZI-b0-vZ+_)V4bUJ$?K?d*=O+ V)JNQ>wgH{Z;OXk;vd$@?2>^tQc!~f3 literal 3870 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 baj7?x%VD!5Cxk2YbYLNVeiXvvpMCWYo=by@ diff --git a/public/index.html b/public/index.html index 7bee027..8c75530 100644 --- a/public/index.html +++ b/public/index.html @@ -19,7 +19,7 @@ work correctly both with client-side routing and a non-root public URL. Learn how to configure a non-root public URL by running `npm run build`. --> - React App + EteSync - Secure, Encryted and Journaled Cloud Sync