├── .eslintrc.json ├── .github ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── README.md └── ROADMAP.md ├── .gitignore ├── .travis.yml ├── LICENSE.md ├── icon.png ├── main.js ├── package.json ├── public └── index.html ├── screenshots └── s1.png ├── src ├── App.css ├── App.js ├── AppContainer.js ├── FAB │ ├── FAB.js │ ├── FABContainer.js │ ├── NewIdeaDialog.js │ ├── NewTaskDialog.css │ ├── NewTaskDialog.js │ ├── __tests__ │ │ ├── FAB.spec.js │ │ ├── NewIdeaDialog.spec.js │ │ └── NewTaskDialog.spec.js │ ├── actionCreators.js │ ├── index.js │ └── lib │ │ ├── __tests__ │ │ └── calculateBottom.spec.js │ │ └── calculateBottom.js ├── Help │ ├── Help.css │ ├── Help.js │ ├── __tests__ │ │ └── Help.spec.js │ └── index.js ├── Idea │ ├── Actions.js │ ├── Animations.css │ ├── ConvertIdeaDialog.css │ ├── ConvertIdeaDialog.js │ ├── EditIdeaDialog.js │ ├── Idea.css │ ├── Idea.js │ ├── IdeaList.css │ ├── IdeaList.js │ ├── IdeaListContainer.js │ ├── __tests__ │ │ ├── Actions.spec.js │ │ ├── ConvertIdeaDialog.spec.js │ │ ├── EditIdeaDialog.spec.js │ │ ├── Idea.spec.js │ │ └── IdeaList.spec.js │ ├── actionCreators.js │ └── index.js ├── Settings │ ├── CalendarSystemDialog.js │ ├── FirstDayOfWeekDialog.js │ ├── Settings.css │ ├── Settings.js │ ├── SettingsContainer.js │ ├── StartupTabDialog.js │ ├── __tests__ │ │ ├── CalendarSystemDialog.spec.js │ │ ├── FirstDayOfWeekDialog.spec.js │ │ ├── Settings.spec.js │ │ └── StartupTabDialog.spec.js │ ├── actionCreators.js │ └── index.js ├── Sidebar │ ├── Sidebar.css │ ├── Sidebar.js │ ├── SidebarContainer.js │ ├── __test__ │ │ └── Sidebar.spec.js │ ├── actionCreators.js │ └── index.js ├── Task │ ├── Actions.js │ ├── Animations.css │ ├── Circle.js │ ├── DueDate.js │ ├── EditTaskDialog.js │ ├── Estimation.js │ ├── Repeat.js │ ├── Task.css │ ├── Task.js │ ├── TaskList.css │ ├── TaskList.js │ ├── TaskListContainer.js │ ├── __tests__ │ │ ├── Actions.spec.js │ │ ├── Circle.spec.js │ │ ├── DueDate.spec.js │ │ ├── EditTaskDialog.spec.js │ │ ├── Estimation.spec.js │ │ ├── Repeat.spec.js │ │ ├── Task.spec.js │ │ └── TaskList.spec.js │ ├── actionCreators.js │ ├── index.js │ └── lib │ │ ├── __tests__ │ │ ├── classify.spec.js │ │ └── cumulate.spec.js │ │ ├── classify.js │ │ └── cumulate.js ├── TitilliumWeb-Regular.ttf ├── __tests__ │ ├── e2e │ │ ├── ideas.e2e.js │ │ ├── settings.e2e.js │ │ └── tasks.e2e.js │ └── reducer.spec.js ├── icons │ ├── __tests__ │ │ └── fork.spec.js │ └── fork.js ├── index.css ├── index.js ├── lib │ ├── __tests__ │ │ └── date.spec.js │ ├── constants.js │ ├── database.js │ ├── date.js │ ├── e2eUtils.js │ └── testUtils.js ├── reducer.js ├── reducers │ ├── __tests__ │ │ ├── appProperties.spec.js │ │ ├── appUI.spec.js │ │ ├── idea.spec.js │ │ └── task.spec.js │ ├── appProperties.js │ ├── appUI.js │ ├── idea.js │ └── task.js ├── setupTests.js └── store.js └── yarn.lock /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "airbnb", 3 | "parser": "babel-eslint", 4 | "rules": { 5 | "react/jsx-filename-extension": [1, { "extensions": [".js", ".jsx"] }], 6 | "react/prop-types": [0], 7 | "no-unused-expressions": ["error", { "allowShortCircuit": true }], 8 | "jsx-a11y/anchor-is-valid": [ "error", { 9 | "components": [ "Link" ], 10 | "specialLink": [ "to" ] 11 | }] 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /.github/CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation. 6 | 7 | ## Our Standards 8 | 9 | Examples of behavior that contributes to creating a positive environment include: 10 | 11 | * Using welcoming and inclusive language 12 | * Being respectful of differing viewpoints and experiences 13 | * Gracefully accepting constructive criticism 14 | * Focusing on what is best for the community 15 | * Showing empathy towards other community members 16 | 17 | Examples of unacceptable behavior by participants include: 18 | 19 | * The use of sexualized language or imagery and unwelcome sexual attention or advances 20 | * Trolling, insulting/derogatory comments, and personal or political attacks 21 | * Public or private harassment 22 | * Publishing others' private information, such as a physical or electronic address, without explicit permission 23 | * Other conduct which could reasonably be considered inappropriate in a professional setting 24 | 25 | ## Our Responsibilities 26 | 27 | Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. 28 | 29 | Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. 30 | 31 | ## Scope 32 | 33 | This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. 34 | 35 | ## Enforcement 36 | 37 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at [Contact Gitter Chat](https://gitter.im/wannachat/contact). The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. 38 | 39 | Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. 40 | 41 | ## Attribution 42 | 43 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version] 44 | 45 | [homepage]: http://contributor-covenant.org 46 | [version]: http://contributor-covenant.org/version/1/4/ 47 | -------------------------------------------------------------------------------- /.github/CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | Contributing to Wanna 2 | ==== 3 | 4 | Welcome, contributor. Let's make the world of open source a better place! :rocket: :rocket: 5 | Wanna welcomes contributions of different types. There are a lot of work to do, and you can help us get them done. 6 | 7 | Types of contribution 8 | ---- 9 | There are multiple kinds of contributions we seek for, ranging from effortless ones like feature suggestion, to harder ones like 10 | app development. Below comes the list of contribution types. Please make sure you have satisfied prerequisties of each contribution type before reading its guidelines, otherwise you may do redundant work. 11 | 12 | ### Feature requests :bulb: 13 | #### Prerequisties: 14 | * Install and use the app 15 | 16 | Feature requests are the easiest type of contribution in Wanna. If you think a feature will make Wanna better, inform us! 17 | To do so, follow these steps: 18 | 1. Search issues and see if anyone has suggested a feature like your one. 19 | 2. If nobody has suggested such a feature, open a new issue and start its title with `[Feature Request] ` (i.e. `[Feature Request] Add support for Persian language`). 20 | 3. Expalain exactly what you are looking for. Provide additional links, images, etc. if needed. 21 | 22 | We may label your issue as `Needs-check`, or we may directly add `idea` or `new-feature` label to your issue, indicating your issue is approved. 23 | 24 | ### Bug reports :beetle: 25 | #### Prerequisties: 26 | * Install and use the app 27 | 28 | If you find a bug when you use the app, you can contribute to Wanna as a bug reporter. To do so, follow these steps which are almost the same as steps for feature requests: 29 | 1. Search issues and see if somebody has reported the same bug. 30 | 2. If the bug is not reported, open an issue and explain about the bug. 31 | 32 | If your issue is labeled as `bug`, we will fix it in the next versions of the app. It may also be labeled as `duplicate` if your issue is a duplicate. 33 | 34 | ### Beginner code contribution :baby: 35 | #### Prerequisties: 36 | * Install and use the app 37 | * Know enough HTML, CSS and basic Javascript 38 | * Familiar with ReactJS and JSX 39 | * Know enough Git and GitHub 40 | 41 | If you want to get started with coding in Wanna, go to the [issues](https://github.com/mkermani144/wanna/issues) and look for the ones labeled `Help-Wanted`. These are usually minor bugfixes or enhancements and you don't need to know full structure of the app to fix them. Usually, they only need basic web development knowledge. To start code contribution, follow these steps: 42 | 1. Look for the issues labeled `Help-Wanted` and choose one. 43 | 2. Comment on the issue indicating you are working on it. 44 | 3. Fork the repo and update your fork code. 45 | 4. After fixing the issue, make a pull request. 46 | 47 | We will review your code. If everything is OK, your PR will be accepted. 48 | 49 | Styleguides 50 | ---- 51 | Although we welcome contributors (and we highly need contributions, certainly), we have some style guides we are very strict about. If a PR does not meet these guidelines, its review will not be approved and we would send you some change requests. So be careful about these guides. 52 | 53 | ### Git 54 | 1. Write short commit messages. GitHub suggests maximum of 50 characters for them. There is some conditions, however, in which you need to write longer messages. 55 | 2. Commit often. Don't put a lot of work in one commit. (If you are using "and" word in your message, you are probably putting a lot of work in your commit!) 56 | 3. Write imperative commit messages (i.e. `Add a feature` instead of `Added a feature`, `Adding a feature`, etc.) 57 | 4. If you are a collaborator, choose appropriate branch names. Use `enhancement/something` when you are adding a new feature, `bugfix/#xyz` when fixing issue `#xyz`, `wip/someLongWorkInProgress` for the works that take a lot of time to complete, etc. 58 | 59 | ### Javascript 60 | 1. Fulfill all of [Airbnb javascript styleguide](https://github.com/airbnb/javascript) (unless we overwrite a rule in `.eslintrc.json` file). We use `eslint-config-airbnb` package to do so. Ideally, config your editor/IDE to help you with this. From their style guides: 61 | * Use semicolons. 62 | * Use 2 spaces for indentation. 63 | * Don't use `var`. Use `const` wherever possible. 64 | * Use signle quotes for strings. 65 | 2. Write functional, declarative code as much as possible. As an example, favor 66 | ```js 67 | const double = x => x * 2; 68 | const data = [1, 2, 3]; 69 | const result = data.map(double); 70 | ``` 71 | over 72 | ```js 73 | const data = [1,2,3]; 74 | const data = []; 75 | for (let i = 0; i < data.length; i++) { 76 | result.push(data[i] * 2); 77 | } 78 | ``` 79 | The first coding styles tells you (the reader of the code) _what_ those lines of code want to do. The second styles, in contrast, tells you _how_ to do that job. It's not the case how to double elements of an array here, however; We just want to do the doubling task. 80 | 3. Embrace ES6 (and the next versions, too). ES6 is the new JS standard. Wanna uses it everywhere. 81 | ### Project directory structure 82 | We mostly use feature-based project structure in Wanna, meaning we group files based on the feature, not the job they do. For example, we group the files in `Task`, `Idea`, `Settings`, etc. directories instead of `ActionCreators`, `Components`, `Containers`. 83 | 84 | Testing 85 | ---- 86 | If you are code contributing in Wanna, do not forget about tests. Tests are vital for every non-trivial software, and Wanna is not an exception here. Next, comes some very brief guidelines about testing, based on our experience. 87 | 88 | ### Unit testing 89 | The first type of testing you have to care about is unit testing. In unit tests, the most important thing to remember is to **test that unit as a black box**. That is, the only thing you have access to when testing a unit (in other words, its input) is its **public API**. (In other words, what you `export` from a javascript module.) You can do everything with that public API, and see if the results are the expected ones. But never, ever access the code inside that unit in your unit tests. The only thing you should use in your unit tests is the public API. 90 | Some general guidelines: 91 | 1. Embrace TDD. Write tests first, and let them drive your development. 92 | 2. Test only one thing. Try to limit your assertions/expectations to one, if possible. 93 | 3. Make the tests as simple as possible. Don't test complex scenarios in unit tests. 94 | 4. Make the tests as small as possible. Use functions to hide details, and focus on actual and expected values in the tests. 95 | 5. Write clear, short unit test messages. 96 | 6. Watch the test fail. An initially-passed test is not so useful. 97 | 98 | #### Unit testing React components 99 | The most challenging units for testing are React components, based on our experience in Wanna development. So carefully read about how to test these units. 100 | As we said earlier, the only thing you have access to when testing a unit is its public API. But what is the public API of a React component? 101 | If you think about it, all of the following are part of a React component public API: 102 | * Its `props`. It is so clear. 103 | * The callable `props` of its children. Those `props` are not related to the component implementation details at all. They are accessible to the outside world of the component as a public API. 104 | * And... its `state`. Yes, the `state` is somehow a public API, **but totally indirectly**. The most important thing is that you never change state manually (i.e. using enzyme `setState()`). **You change state using the public API** (the two previous ones). So it's better to say `state` is not a public API itself, but it can be changed using the public API. (Pay close attention to this. If there are some `state` in the component that is changed *only inside the component itself*, it is an implementation detail and we do not care about it in our test.) But because a React component output is subject to `state` changes, this one is worth to be mentioned as the third way you can change a component input. (As we suggested as a general guideline, unit tests should be as simple as possible. Therefore don't implement multiple `state` transition in the unit tests. At most, you should have one `state` transition in your unit tests. Complicated `state` transitions shouldn't be unit tested.) 105 | 106 | So, when you want to unit test a React component, you set different `props`, call its children callable `props` or change its `state` using its `props` or calling its children `props`, and check if the result is as expected. 107 | 108 | These are all anti-patterns: 109 | * Using enzyme `wrapper.instance()` 110 | * Calling the functions inside components that are not accessible from the outside (If you need to unit test these functions, put them in a separate module and test that module instead) 111 | * Using enzyme `setState()` 112 | * Having multiple `expect`s in a unit test 113 | 114 | ### e2e testing 115 | The second type of tests we use in Wanna are e2e tests. As we said earlier, complex scenarios should not be tested using unit tests; instead, e2e tests are the tool to do that job. An e2e test simulates interaction of the user with the app (e.g. mouse clicks, keyboard inputs, etc.). We use `selenium-webdriver` (without any wrapper library) in our e2e tests. 116 | 117 | Some general guidelines: 118 | 1. Test only one scenario (e.g. only test app settings functionality in an individual e2e test). 119 | 2. Divide the test into multiple sub-scenarios, and check if the result in the end of each sub-scenario is as expected. 120 | 3. Don't repeat yourself. There are some tasks (like mouse clicks, keyboard inputs, etc.) that are used again and again in your tests. Separate these kinds of tasks in another module and make the test more readable. 121 | 4. Run the test yourself, and watch it in action. 122 | 5. Use large timeouts in the tests. Don't assume the tests are run as fast as your computer. Some e2e tests may take several minutes to complete, while the same ones may run in half a minute in your computer. 123 | 6. Wait often. If the test fails and your code is correct (and you don't know why, as the famous meme suggests :joy:), you may need to wait between two lines of your e2e test. 124 | 7. Use appropriate `class`es and `id`s in your React components to simplify CSS selectors in your e2e tests. 125 | 126 | Misc 127 | --- 128 | 129 | ### Dates 130 | We have our own tiny module for manipulating dates. This module is located in `src/lib/date.js`. Try to use its apis for all kinds of date and time manipulations. 131 | Except `parse` function, which exactly does the same thing as `Date.parse`, all other exported functions in this module use unix times as input (if any) and return unix times as output, too. Javascript date objects are neither provided as inputs to these functions nor returned as their output. 132 | -------------------------------------------------------------------------------- /.github/README.md: -------------------------------------------------------------------------------- 1 | asdf 2 | 3 | Wanna 4 | ==== 5 | [![Build Status](https://img.shields.io/travis/mkermani144/wanna.svg)](https://travis-ci.org/mkermani144/wanna) 6 | [![Downloads](https://img.shields.io/github/downloads/mkermani144/wanna/total.svg)]() 7 | [![Release](https://img.shields.io/github/release/mkermani144/wanna.svg)]() 8 | [![Issues](https://img.shields.io/github/issues-raw/mkermani144/wanna.svg)]() 9 | [![Pull requests](https://img.shields.io/github/issues-pr-raw/mkermani144/wanna.svg)]() 10 | [![Wannachat](https://badges.gitter.im/wannachat/Lobby.svg)](https://gitter.im/wannachat/Lobby?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) 11 | 12 | Screenshot 13 | ---- 14 | ![screenshot](../screenshots/s1.png) 15 | 16 | Table of contents 17 | ---- 18 | - [Introduction](#introduction) 19 | - [Installation](#installation) 20 | - [Tutorials](#tutorials) 21 | - [Community](#community) 22 | - [Contributing](#contributing) 23 | - [Issues and bug reports](#issues-and-bug-reports) 24 | - [Technologies and libraries](#technologies-and-libraries) 25 | - [Philosophy](#philosophy) 26 | - [Workflow](#workflow) 27 | - [License](#license) 28 | 29 | Introduction 30 | ---- 31 | Wanna is an implementation of a 21st-century to-do list app. It introduces a [new workflow](#workflow) and has its own [philosophy](#philosophy) which makes it different from other to-do list apps. 32 | 33 | Installation 34 | ---- 35 | Wanna is under active development. You can see a list of its releases [here](https://github.com/mkermani144/wanna/releases). At this time, the latest release of the app is [Flex Alpha 2 release](https://github.com/mkermani144/wanna/releases/tag/1.0.0-alpha.2%2Bflex) for Mac, Windows and Linux (`.deb` only). In addition, you can easily download the repo's source code and build a version suitable for your own platform with just a bit of effort. In the near future, other platforms will be officially supported. 36 | You can also clone the repository to see development version of the app in action: 37 | ```sh 38 | git clone https://github.com/mkermani144/wanna.git 39 | cd wanna/desktop 40 | yarn && yarn electron # Or `npm i && npm electron` 41 | ``` 42 | 43 | Tutorials 44 | ---- 45 | _Tutorials will be added soon._ 46 | 47 | Community 48 | ---- 49 | Join [Wannachat](https://gitter.im/wannachat/Lobby) on Gitter! 50 | 51 | Contributing 52 | ---- 53 | Please see [contributing guidelines](CONTRIBUTING.md) for a comprehensive description of how to contribute to Wanna. 54 | 55 | Issues and bug reports 56 | ---- 57 | Please see [contribution types](CONTRIBUTING.md#types-of-contribution) that we are looking for in Wanna, including feature requests and bug reports. 58 | 59 | Road map 60 | ---- 61 | If you want to see a unofficial list of features which will be added in the next versions of Wanna, take a look at our [road map](ROADMAP.md). 62 | 63 | Technologies and libraries 64 | ---- 65 | Like the other apps and libraries, Wanna stands on the shoulders of giants. This is a list of mostly used awesome technologies and libraries that power Wanna: 66 | - [Electron](https://electron.atom.io): Build cross platform desktop apps with JavaScript, HTML, and CSS. 67 | - [React.js](https://facebook.github.io/react/): A declarative, efficient, and flexible JavaScript library for building user interfaces. 68 | - [Material-UI](http://www.material-ui.com): React Components that Implement Google's Material Design. 69 | - [Create-react-app](https://github.com/facebookincubator/create-react-app): Create React apps with no build configuration. 70 | - [React-Router](https://github.com/ReactTraining/react-router): Declarative routing for React. 71 | - [Redux](http://redux.js.org): Predictable state container for JavaScript apps. 72 | - [Eslint](http://eslint.org): A fully pluggable tool for identifying and reporting on patterns in JavaScript. 73 | 74 | In addition, these libraries and technologies are used in the repo GitHub pages: 75 | - [Bootstrap](https://v4-alpha.getbootstrap.com): The most popular HTML, CSS, and JavaScript framework for developing responsive, mobile first projects on the web. 76 | - [Now UI kit](https://github.com/creativetimofficial/now-ui-kit): Now UI Kit Bootstrap 4 - Designed by Invision. Coded by Creative Tim. 77 | - [Font awesome](http://fontawesome.io): The iconic font and CSS toolkit. 78 | - [BrowserStack](https://browserstack.com): Live, web-based browser testing. 79 | - [dns.js.org](https://github.com/js-org/dns.js.org): Providing nice and free domains for GitHub Pages since 2015. 80 | 81 | Philosophy 82 | ---- 83 | [Every time one builds a to-do list app, a puppy dies.](https://medium.freecodecamp.com/every-time-you-build-a-to-do-list-app-a-puppy-dies-505b54637a5d) So why should Wanna exist? 84 | There are many to-do list apps out there. All have pros and cons and may or may not work for you. But nearly all of them lack one critical feature: they are just a digital version of paper to-do lists. You throw some tasks into them, and then, whether you complete the task or not, nothing great happens: The app is somehow passive. 85 | Wanna _tries_ to add some features that make it more active. It tries to award you in some manner if you complete your tasks. It helps you get back to your work if you fail. It attempts to motivate you to be productive. It keeps a bank of your ideas. In essence, __Wanna tries to be smart and react to your activities.__ (Note that these features are not entirely available in Wanna hitherto, but it will evolve and get better gradually.) 86 | 87 | (Don't forget: Wanna is not magic. It's just an application. As a human, if you don't want to improve, if you don't want to get your tasks done, you can easily cheat the app (and yourself), and Wanna cannot help you anyway.) 88 | 89 | Workflow 90 | ---- 91 | Wanna workflow is dead simple; in brief, an idea comes to your mind, you save it in Wanna, convert it to some tasks and finally do it: 92 | 93 | 1. An idea occurs to you. It can be any type of idea; listening to a great music, doing your school homework, plan for running, reading a book, learning a new programming language, trying always to smile, launching a small party with your family and friends or watching a TED talk are some examples. 94 | 95 | 2. You add the idea to your ideas list. You don't need to care about when to do it. It's just an idea, not a task. 96 | 97 | 3. Now you have a mess of ideas. You can scroll up and down and pick one of them out of your list. 98 | 99 | 4. Once you selected the idea, it's time to convert it to some tasks. You have to set a period of time in which each task should be done. Moreover, you have to estimate the time that task will take. (Note that you can skip the previous three sections and directly add a task.) 100 | 101 | 5. Having your tasks added to your list, Wanna shows each task with a colorful status circle. The more this color tends to become red, the closer the task due date is. Don't let those circles turn red! 102 | 103 | License 104 | ---- 105 | MIT license, copyright (c) 2017 Mohammad Kermani 106 | -------------------------------------------------------------------------------- /.github/ROADMAP.md: -------------------------------------------------------------------------------- 1 | Road map 2 | ==== 3 | 4 | **Disclaimer: This is not a mission statement. We don't guarantee to develop Wanna exactly based on this roadmap. This file is subject to change in the future, and some new features could be added, or existing ones may be updated, postponed or even totally dropped.** 5 | 6 | Wanna is growing, and a lot of features are out there to be added to it. So we have to prioritize them and work on the most important parts first. Here comes the road map of Wanna. 7 | 8 | Wanna Flex Beta 9 | ---- 10 | The next version of the app, Flex Beta, will be focused on testing, performance, animations (and flexibility, for sure!): 11 | - [X] **Testing:** Wanna Flex Alpha was completed without writing any line of tests. A software without tests is not a good software. So the first concern in Flex Beta version will be adding tests. 12 | - [X] **Performance:** Although React is developed with performance considerations in the head, Wanna is still a bit slow. So performance improvement becomes an important case. 13 | - [x] **Animations:** Let's be reckless. No matter how beautiful Wanna becomes and implement Google Material Design better, it is still ugly without animations. Just adding very basic animations will fulfill the user need for animations. 14 | - [ ] **Flexibility:** Wait! There is more. This version of Wanna is named **Flex**, as it aims at giving the user *flexibility* in getting his/her tasks done. This is the most important feature to be implemented (but the last one, too, as previous features are vital for the app, and should not be postponed). 15 | 16 | Wanna Flex 17 | ---- 18 | And then comes Wanna Flex, the first official, not pre-release of the app, with new features in mind! Syncing, Wanna PWA, and Internationalization will be the most important ones. 19 | - [ ] **Syncing:** Syncing is truly crucial for Wanna. You add a task in your laptop, then go to your work, turn on your PC at your office and open Wanna. Ah, your new task is not in *that* Wanna. It really, really sucks. (The problem here is the need for some servers, however, and most servers are not free! This is a big bottleneck.) 20 | - [ ] **Wanna Progressive Web App (PWA):** Writing Wanna native apps need lots of effort. Even by using `React native`, it takes a lot of time (or better, it's impossible) to implement Google Material Design for `react-native`, as `material-ui` (the library used to give the desktop app Material Design look) is not available for `react-native` at this time. In opposite, converting current desktop implementation of Wanna to a PWA requires small tweaks, and can be done faster. So there is an intention to introduce Wanna PWA in Flex version. (Like syncing, however, this feature is dependent on the availability of some servers.) 21 | - [ ] **Internationalization:** At this time, the only language Wanna officially support is English. It will be nice to add more languages to the app, in order to satisfy a broader community of the users from all over the world. 22 | 23 | What's next? 24 | ---- 25 | Flex is not the end. There will be a lot of other features in the next versions of the app, including: 26 | - [ ] **AI:** Based on Wanna philosophy, AI is one the most important things to be added to the app. (It is not exactly clear how AI can be used in Wanna, but as an example, AI powered Wanna may suggest you due dates or time estimations for your tasks, helping you become more productive.) 27 | - [ ] **Projects:** Grouping tasks into projects will help you get more organized about your work. Wanna will benefit from the projects in the future. 28 | - [ ] **Reports:** Seeing a comprehensive report of the things you got done will make you more motivated about your work and help you identify your strengths and weaknesses in a more visual way. 29 | - [ ] **Connect:** Getting your tasks done is more enjoyable when there is a community. In the next versions, you can get in touch with others from all over the world, collaborate or compete with them, share your achievements and more. 30 | 31 | *Note: The above features need plenty of time and resources. Although we do our best in developing Wanna, some of the mentioned features may not become practical and may be totally dropped, as we said earlier.* 32 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | 6 | # testing 7 | /coverage 8 | 9 | # production 10 | /build 11 | 12 | # misc 13 | .DS_Store 14 | .env.local 15 | .env.development.local 16 | .env.test.local 17 | .env.production.local 18 | package-lock.json 19 | 20 | npm-debug.log* 21 | yarn-debug.log* 22 | yarn-error.log* 23 | .yarnclean 24 | .wanna/ 25 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | dist: trusty 2 | sudo: required 3 | language: node_js 4 | node_js: 5 | - '8' 6 | addons: 7 | chrome: stable 8 | before_install: 9 | - wget "http://chromedriver.storage.googleapis.com/2.35/chromedriver_linux64.zip" 10 | - unzip chromedriver_linux64.zip -d $HOME/bin 11 | - export PATH=$PATH:$HOME/bin 12 | - npm i -g yarn 13 | install: 14 | - npm i -g codecov 15 | before_script: 16 | - "export DISPLAY=:99.0" 17 | - "sh -e /etc/init.d/xvfb start" 18 | - sleep 3 19 | script: 20 | - yarn && yarn test:cov && yarn start-and-e2e -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Mohammad Kermani 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mkermani144/wanna/7973a3d80825c99cb31d6a89e436fae075c2c7f7/icon.png -------------------------------------------------------------------------------- /main.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const path = require('path'); 3 | const os = require('os'); 4 | const electron = require('electron'); 5 | 6 | const { app } = electron; 7 | const { BrowserWindow } = electron; 8 | 9 | let parentPath = os.homedir(); 10 | if (process.env.NODE_ENV === 'development') { 11 | parentPath = './'; 12 | } 13 | 14 | 15 | const dbPath = path.join(parentPath, '.wanna/db'); 16 | 17 | const dbExists = () => { 18 | if (fs.existsSync(dbPath)) { 19 | return true; 20 | } 21 | return false; 22 | }; 23 | 24 | const createDatabase = () => { 25 | const wannaDirectoryPath = path.join(parentPath, '.wanna'); 26 | fs.mkdirSync(wannaDirectoryPath); 27 | const prefill = JSON.stringify({ 28 | tasks: [], 29 | ideas: [], 30 | appProperties: { 31 | showNotYetTasks: true, 32 | fullscreen: false, 33 | calendarSystem: 'en-US', 34 | firstDayOfWeek: 1, 35 | startupTab: 'tasks', 36 | }, 37 | }); 38 | fs.writeFileSync(dbPath, prefill); 39 | }; 40 | 41 | const isFullscreen = () => { 42 | const data = fs.readFileSync(dbPath, 'utf-8'); 43 | return JSON.parse(data).appProperties.fullscreen; 44 | }; 45 | 46 | let win; 47 | 48 | function createWindow() { 49 | if (dbExists() === false) { 50 | createDatabase(); 51 | } 52 | let width = 1024; 53 | let height = 768; 54 | if (isFullscreen()) { 55 | ({ width, height } = electron.screen.getPrimaryDisplay().size); 56 | } 57 | win = new BrowserWindow({ 58 | minWidth: 800, 59 | minHeight: 600, 60 | width, 61 | height, 62 | icon: `${__dirname}/icon.png`, 63 | }); 64 | win.loadURL('http://localhost:3000'); 65 | } 66 | 67 | app.on('ready', () => { 68 | createWindow(); 69 | }); 70 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "wanna", 3 | "version": "1.0.0-alpha.2+flex", 4 | "description": "An implementation of a 21st-century to-do list app", 5 | "homepage": "https://wanna.js.org", 6 | "bugs": { 7 | "url": "https://github.com/mkermani144/wanna/issues" 8 | }, 9 | "license": "MIT", 10 | "author": "Mohammad Kermani", 11 | "repository": "github:mkermani144/wanna", 12 | "main": "main.js", 13 | "devDependencies": { 14 | "enzyme": "^3.3.0", 15 | "enzyme-adapter-react-16": "^1.1.1", 16 | "eslint": "^4.16.0", 17 | "eslint-config-airbnb": "^16.1.0", 18 | "eslint-plugin-import": "^2.6.0", 19 | "eslint-plugin-jsx-a11y": "^6.0.3", 20 | "eslint-plugin-react": "^7.1.0", 21 | "husky": "^0.14.3", 22 | "npm-run-all": "^4.1.1", 23 | "react-scripts": "1.1.0", 24 | "react-test-renderer": "^16.2.0", 25 | "selenium-webdriver": "^3.6.0" 26 | }, 27 | "dependencies": { 28 | "cross-env": "^5.0.5", 29 | "electron": "^1.7.11", 30 | "material-ui": "0.20.0", 31 | "material-ui-persian-date-picker-utils": "^0.1.2", 32 | "ramda": "^0.25.0", 33 | "react": "^16.2.0", 34 | "react-addons-css-transition-group": "^15.6.2", 35 | "react-dom": "^16.2.0", 36 | "react-hotkeys": "^0.10.0", 37 | "react-redux": "^5.0.5", 38 | "react-router": "^4.1.1", 39 | "react-router-dom": "^4.1.1", 40 | "react-tap-event-plugin": "^3.0.2", 41 | "redux": "^3.6.0", 42 | "redux-undo": "^0.6.1", 43 | "shortid": "^2.2.8" 44 | }, 45 | "scripts": { 46 | "start": "react-scripts start", 47 | "start:nodeless": "cross-env REACT_APP_E2E=true BROWSER=none react-scripts start", 48 | "build": "react-scripts build", 49 | "test": "react-scripts test --testPathPattern='(.*/)?.+\\.spec\\.js'", 50 | "test:e2e": "react-scripts test --testPathPattern='(.*/)?.+\\.e2e\\.js'", 51 | "test:cov": "yarn test --coverage && codecov", 52 | "lint": "eslint main.js src/", 53 | "eject": "react-scripts eject", 54 | "electron": "cross-env NODE_ENV=development electron . | cross-env BROWSER=none react-scripts start", 55 | "precommit": "yarn lint && cross-env CI=true yarn test --onlyChanged", 56 | "start-and-e2e": "cross-env CI=true npm-run-all -r -p test:e2e start:nodeless" 57 | }, 58 | "jest": { 59 | "collectCoverageFrom": [ 60 | "src/**/*.{js,jsx}", 61 | "!/node_modules/", 62 | "!src/**/*Container.js", 63 | "!src/index.js", 64 | "!src/store.js", 65 | "!src/lib/database.js" 66 | ] 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Wanna 6 | 7 | 8 |
9 | 10 | 11 | -------------------------------------------------------------------------------- /screenshots/s1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mkermani144/wanna/7973a3d80825c99cb31d6a89e436fae075c2c7f7/screenshots/s1.png -------------------------------------------------------------------------------- /src/App.css: -------------------------------------------------------------------------------- 1 | .App { 2 | display: flex; 3 | flex-direction: column; 4 | width: 100vw; 5 | height: 100vh; 6 | overflow-x: hidden; 7 | background-color: #FAFAFA; 8 | } 9 | .App .main { 10 | flex: 1; 11 | display: flex; 12 | flex-direction: row; 13 | } 14 | .AppBar { 15 | position: fixed !important; 16 | cursor: default; 17 | } 18 | * { 19 | user-select: none; 20 | } 21 | 22 | *::-webkit-scrollbar-track 23 | { 24 | -webkit-box-shadow: inset 0 0 5px rgba(0,0,0,0.2); 25 | background-color: #E3F2FD; 26 | } 27 | *::-webkit-scrollbar 28 | { 29 | width: 5px; 30 | background-color: #E3F2FD; 31 | } 32 | *::-webkit-scrollbar-thumb 33 | { 34 | background-color: #64B5F6; 35 | border-radius: 3px; 36 | margin-right: 3px; 37 | } 38 | -------------------------------------------------------------------------------- /src/App.js: -------------------------------------------------------------------------------- 1 | /* eslint-env browser */ 2 | 3 | import React, { Component } from 'react'; 4 | import { Provider } from 'react-redux'; 5 | import { Redirect, Route, BrowserRouter as Router } from 'react-router-dom'; 6 | import MuiThemeProvider from 'material-ui/styles/MuiThemeProvider'; 7 | import getMuiTheme from 'material-ui/styles/getMuiTheme'; 8 | import AppBar from 'material-ui/AppBar'; 9 | import injectTapEventPlugin from 'react-tap-event-plugin'; 10 | import { blue500, green800, pink300 } from 'material-ui/styles/colors'; 11 | import { HotKeys } from 'react-hotkeys'; 12 | 13 | import store from './store'; 14 | import SidebarContainer from './Sidebar'; 15 | import FABContainer from './FAB'; 16 | import TaskListContainer from './Task'; 17 | import IdeaListContainer from './Idea'; 18 | import SettingsContainer from './Settings'; 19 | import Help from './Help'; 20 | 21 | import { update } from './lib/database'; 22 | 23 | import './App.css'; 24 | 25 | injectTapEventPlugin(); 26 | 27 | class App extends Component { 28 | state = { 29 | toTasks: false, 30 | toIdeas: false, 31 | toSettings: false, 32 | toHelp: false, 33 | sidebarExpanded: false, 34 | }; 35 | keyMap = { 36 | showTasks: 'shift+t', 37 | showIdeas: 'shift+i', 38 | showSettings: 'shift+s', 39 | showHelp: 'shift+h', 40 | }; 41 | 42 | handleSidebarToggle = () => { 43 | this.setState(prevState => ({ sidebarExpanded: !prevState.sidebarExpanded })); 44 | } 45 | render() { 46 | const muiTheme = getMuiTheme({ 47 | palette: { 48 | primary1Color: blue500, 49 | }, 50 | datePicker: { 51 | selectColor: green800, 52 | headerColor: green800, 53 | }, 54 | snackbar: { 55 | actionColor: pink300, 56 | }, 57 | }); 58 | store.subscribe(() => { 59 | update(store.getState()); 60 | }); 61 | const handlers = { 62 | showTasks: () => { 63 | this.props.changeTab('tasks'); 64 | this.setState({ 65 | toTasks: true, 66 | }, () => { 67 | this.setState({ 68 | toTasks: false, 69 | }); 70 | }); 71 | }, 72 | showIdeas: () => { 73 | this.props.changeTab('ideas'); 74 | this.setState({ 75 | toIdeas: true, 76 | }, () => { 77 | this.setState({ 78 | toIdeas: false, 79 | }); 80 | }); 81 | }, 82 | showSettings: () => { 83 | this.props.changeTab('settings'); 84 | this.setState({ 85 | toSettings: true, 86 | }, () => { 87 | this.setState({ 88 | toSettings: false, 89 | }); 90 | }); 91 | }, 92 | showHelp: () => { 93 | this.props.changeTab('help'); 94 | this.setState({ 95 | toHelp: true, 96 | }, () => { 97 | this.setState({ 98 | toHelp: false, 99 | }); 100 | }); 101 | }, 102 | }; 103 | return ( 104 | 105 | 111 | 112 | 113 |
114 | 119 |
120 | 121 | 125 | 126 | {this.state.toTasks && 127 | 128 | } 129 | {this.state.toIdeas && 130 | 131 | } 132 | {this.state.toSettings && 133 | 134 | } 135 | {this.state.toHelp && 136 | 137 | } 138 | 141 | () 142 | } 143 | /> 144 | 147 | () 148 | } 149 | /> 150 | 153 | () 154 | } 155 | /> 156 | 159 | ( 160 | 164 | ) 165 | } 166 | /> 167 |
168 |
169 |
170 |
171 |
172 |
173 | ); 174 | } 175 | } 176 | 177 | export default App; 178 | -------------------------------------------------------------------------------- /src/AppContainer.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { connect } from 'react-redux'; 3 | import { bindActionCreators } from 'redux'; 4 | 5 | import changeTab from './Sidebar/actionCreators'; 6 | import App from './App'; 7 | import store from './store'; 8 | 9 | const mapStateToProps = state => ({ 10 | currentTab: state.appUI.currentTab, 11 | }); 12 | 13 | const mapDispatchToProps = dispatch => bindActionCreators({ changeTab }, dispatch); 14 | 15 | const AppContainer = connect(mapStateToProps, mapDispatchToProps)(App); 16 | 17 | const AppContainerWithStore = props => ( 18 | 19 | ); 20 | 21 | export default AppContainerWithStore; 22 | -------------------------------------------------------------------------------- /src/FAB/FAB.js: -------------------------------------------------------------------------------- 1 | /* eslint-env browser */ 2 | 3 | import React, { Component } from 'react'; 4 | // import { SpeedDial, SpeedDialItem } from 'react-mui-speeddial'; 5 | import FloatingActionButton from 'material-ui/FloatingActionButton'; 6 | import Add from 'material-ui/svg-icons/content/add'; 7 | import Done from 'material-ui/svg-icons/action/done'; 8 | import LightbulbOutline from 'material-ui/svg-icons/action/lightbulb-outline'; 9 | import { green600, yellow800 } from 'material-ui/styles/colors'; 10 | import shortid from 'shortid'; 11 | import { HotKeys } from 'react-hotkeys'; 12 | 13 | import NewTaskDialog from './NewTaskDialog'; 14 | import NewIdeaDialog from './NewIdeaDialog'; 15 | import calculateBottom from './lib/calculateBottom'; 16 | 17 | import { 18 | FABRaiseWindowWidthTreshold, 19 | FABBottom, 20 | FABRaisedBottom, 21 | FABMiniRaisedBottomClosed, 22 | FABRight, 23 | FABMiniRight, 24 | } from '../lib/constants'; 25 | 26 | 27 | class FAB extends Component { 28 | state = { 29 | taskDialogOpen: false, 30 | ideaDialogOpen: false, 31 | FABOpen: false, 32 | }; 33 | keyMap = { 34 | addNewIdea: 'ctrl+i', 35 | addNewTask: 'ctrl+t', 36 | }; 37 | 38 | handleToggleFAB = () => { 39 | this.setState(prev => ({ 40 | FABOpen: !prev.FABOpen, 41 | })); 42 | } 43 | handleRequestClose = () => { 44 | this.setState({ 45 | taskDialogOpen: false, 46 | ideaDialogOpen: false, 47 | }); 48 | } 49 | handleRequestTaskDialogOpen = () => { 50 | this.setState({ 51 | FABOpen: false, 52 | taskDialogOpen: true, 53 | }); 54 | } 55 | handleRequestIdeaDialogOpen = () => { 56 | this.setState({ 57 | FABOpen: false, 58 | ideaDialogOpen: true, 59 | }); 60 | } 61 | handleRequestTaskAdd = (taskInfo) => { 62 | const repetitionDays = taskInfo.repetition * taskInfo.repetitionValue; 63 | const id = shortid.generate(); 64 | this.props.addTask({ 65 | task: taskInfo.task, 66 | start: taskInfo.start, 67 | end: taskInfo.end, 68 | estimation: taskInfo.estimation * taskInfo.estimationValue, 69 | repetition: repetitionDays, 70 | done: false, 71 | id, 72 | }); 73 | } 74 | handleRequestIdeaAdd = (ideaInfo) => { 75 | const id = shortid.generate(); 76 | this.props.addIdea({ 77 | idea: ideaInfo.idea, 78 | id, 79 | }); 80 | } 81 | render() { 82 | const styles = { 83 | plusFAB: { 84 | position: 'absolute', 85 | right: FABRight, 86 | bottom: this.props.FABRaised && this.props.width < FABRaiseWindowWidthTreshold ? 87 | FABRaisedBottom : 88 | FABBottom, 89 | transform: this.state.FABOpen ? 'rotate(45deg)' : 'rotate(0)', 90 | zIndex: 1000, 91 | transition: 'all 400ms cubic-bezier(0.23, 1, 0.32, 1) 0ms', 92 | }, 93 | doneFAB: { 94 | position: 'absolute', 95 | right: FABMiniRight, 96 | bottom: this.props.FABRaised && this.props.width < FABRaiseWindowWidthTreshold ? 97 | FABMiniRaisedBottomClosed : 98 | calculateBottom(this.state.FABOpen, 0), 99 | opacity: this.state.FABOpen ? 1 : 0, 100 | zIndex: 999, 101 | transition: 'all 400ms cubic-bezier(0.23, 1, 0.32, 1) 0ms', 102 | }, 103 | lightbulbFAB: { 104 | position: 'absolute', 105 | right: FABMiniRight, 106 | bottom: this.props.FABRaised && this.props.width < FABRaiseWindowWidthTreshold ? 107 | FABMiniRaisedBottomClosed : 108 | calculateBottom(this.state.FABOpen, 1), 109 | opacity: this.state.FABOpen ? 1 : 0, 110 | zIndex: 999, 111 | transition: 'all 400ms cubic-bezier(0.23, 1, 0.32, 1) 0ms', 112 | }, 113 | newTask: { 114 | color: green600, 115 | }, 116 | newIdea: { 117 | color: yellow800, 118 | }, 119 | }; 120 | const handlers = { 121 | addNewIdea: this.handleRequestIdeaDialogOpen, 122 | addNewTask: this.handleRequestTaskDialogOpen, 123 | }; 124 | return ( 125 | 131 | 136 | 137 | 138 | 145 | 146 | 147 | 154 | 155 | 156 | 163 | 168 | 169 | ); 170 | } 171 | } 172 | 173 | export default FAB; 174 | -------------------------------------------------------------------------------- /src/FAB/FABContainer.js: -------------------------------------------------------------------------------- 1 | import { connect } from 'react-redux'; 2 | import { bindActionCreators } from 'redux'; 3 | 4 | import * as actionCreators from './actionCreators'; 5 | 6 | import FAB from './FAB'; 7 | 8 | const mapStateToProps = state => ({ 9 | calendarSystem: state.appProperties.calendarSystem, 10 | firstDayOfWeek: state.appProperties.firstDayOfWeek, 11 | FABRaised: state.appUI.FABRaised, 12 | }); 13 | 14 | const mapDispatchToProps = dispatch => bindActionCreators(actionCreators, dispatch); 15 | 16 | const FABContainer = connect(mapStateToProps, mapDispatchToProps)(FAB); 17 | 18 | export default FABContainer; 19 | -------------------------------------------------------------------------------- /src/FAB/NewIdeaDialog.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | 3 | import Dialog from 'material-ui/Dialog'; 4 | import FlatButton from 'material-ui/FlatButton'; 5 | import TextField from 'material-ui/TextField'; 6 | import { yellow800, grey50 } from 'material-ui/styles/colors'; 7 | import { HotKeys } from 'react-hotkeys'; 8 | 9 | class NewIdeaDialog extends Component { 10 | state = { idea: '' }; 11 | keyMap = { 12 | confirmAddNewIdeaAndFinish: 'shift+enter', 13 | confirmAddNewIdeaAndContinue: 'enter', 14 | }; 15 | 16 | handleIdeaChange = (e) => { 17 | this.setState({ 18 | idea: e.target.value, 19 | }); 20 | } 21 | handleRequestClose = () => { 22 | this.setState({ idea: '' }); 23 | this.props.onRequestClose(); 24 | } 25 | handleRequestAdd = () => { 26 | this.props.onRequestAdd(this.state); 27 | this.setState({ idea: '' }); 28 | } 29 | handleRequestFinish = () => { 30 | this.handleRequestAdd(); 31 | this.handleRequestClose(); 32 | } 33 | render() { 34 | const actions = [ 35 | , 42 | , 49 | , 54 | ]; 55 | const dialogTitleStyle = { 56 | backgroundColor: yellow800, 57 | color: grey50, 58 | cursor: 'default', 59 | }; 60 | const textFieldStyles = { 61 | underlineFocusStyle: { 62 | borderColor: yellow800, 63 | }, 64 | floatingLabelFocusStyle: { 65 | color: yellow800, 66 | }, 67 | }; 68 | const handlers = { 69 | confirmAddNewIdeaAndFinish: () => { 70 | this.state.idea && this.handleRequestFinish(); 71 | }, 72 | confirmAddNewIdeaAndContinue: () => { 73 | this.state.idea && this.handleRequestAdd(); 74 | }, 75 | }; 76 | return ( 77 | 85 |
86 |

Do you have an idea?

87 |
88 | 92 | 101 | 102 |
103 | ); 104 | } 105 | } 106 | 107 | export default NewIdeaDialog; 108 | -------------------------------------------------------------------------------- /src/FAB/NewTaskDialog.css: -------------------------------------------------------------------------------- 1 | .textfields > * { 2 | display: flex; 3 | flex-flow: row wrap; 4 | align-items: center; 5 | justify-content: space-between; 6 | } 7 | .datepicker { 8 | width: 48%; 9 | display: flex; 10 | } 11 | .datepicker > div { 12 | flex-grow: 1; 13 | display: flex; 14 | } 15 | .datepicker > div > div:first-of-type { 16 | flex-grow: 1; 17 | } 18 | .row { 19 | width: 50%; 20 | display: flex; 21 | flex-direction: row; 22 | align-items: center; 23 | } 24 | .row > div:first-of-type { 25 | width: 60% !important; 26 | } 27 | .row > div:last-of-type { 28 | width: 40% !important; 29 | } 30 | -------------------------------------------------------------------------------- /src/FAB/NewTaskDialog.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import Dialog from 'material-ui/Dialog'; 3 | import FlatButton from 'material-ui/FlatButton'; 4 | import DatePicker from 'material-ui/DatePicker'; 5 | import TextField from 'material-ui/TextField'; 6 | import DropDownMenu from 'material-ui/DropDownMenu'; 7 | import MenuItem from 'material-ui/MenuItem'; 8 | import { green600, grey50 } from 'material-ui/styles/colors'; 9 | import persianUtils from 'material-ui-persian-date-picker-utils'; 10 | import { HotKeys } from 'react-hotkeys'; 11 | 12 | import { 13 | parse, 14 | dayStart, 15 | todayStart, 16 | dayEnd, 17 | } from '../lib/date'; 18 | 19 | import './NewTaskDialog.css'; 20 | 21 | class NewTaskDialog extends Component { 22 | state = { 23 | estimationValue: 1, 24 | repetitionValue: 1, 25 | task: '', 26 | start: todayStart(), 27 | end: null, 28 | estimation: '', 29 | repetition: '', 30 | }; 31 | keyMap = { 32 | confirmAddNewTaskAndFinish: 'shift+enter', 33 | confirmAddNewTaskAndContinue: 'enter', 34 | }; 35 | 36 | buttonDisabled = () => !( 37 | this.state.task 38 | && this.state.end 39 | && this.state.estimation 40 | && /^[0-9]*$/.test(this.state.estimation) 41 | && /^[0-9]*$/.test(this.state.repetition) 42 | ); 43 | handleEstimationMenuChange = (e, i, value) => { 44 | this.setState({ 45 | estimationValue: value, 46 | }); 47 | } 48 | handleRepetitionMenuChange = (e, i, value) => { 49 | this.setState({ 50 | repetitionValue: value, 51 | }); 52 | } 53 | handleTaskChange = (e) => { 54 | this.setState({ 55 | task: e.target.value, 56 | }); 57 | } 58 | handleStartChange = (e, start) => { 59 | this.setState({ 60 | start: dayStart(parse(start)), 61 | }); 62 | } 63 | handleEndChange = (e, end) => { 64 | this.setState({ 65 | end: dayEnd(parse(end)), 66 | }); 67 | } 68 | handleEstimationChange = (e) => { 69 | this.setState({ 70 | estimation: e.target.value, 71 | }); 72 | } 73 | handleRepetitionChange = (e) => { 74 | this.setState({ 75 | repetition: e.target.value, 76 | }); 77 | } 78 | handleRequestClose = () => { 79 | this.setState({ 80 | estimationValue: 1, 81 | repetitionValue: 1, 82 | task: '', 83 | start: todayStart(), 84 | end: null, 85 | estimation: '', 86 | repetition: '', 87 | }); 88 | this.props.onRequestClose(); 89 | } 90 | handleRequestAdd = () => { 91 | this.props.onRequestAdd(this.state); 92 | this.setState({ 93 | estimationValue: 1, 94 | repetitionValue: 1, 95 | task: '', 96 | start: todayStart(), 97 | end: null, 98 | estimation: '', 99 | repetition: '', 100 | }); 101 | } 102 | handleRequestFinish = () => { 103 | this.handleRequestAdd(); 104 | this.handleRequestClose(); 105 | } 106 | render() { 107 | const actions = [ 108 | , 115 | , 122 | , 127 | ]; 128 | const dialogTitleStyle = { 129 | backgroundColor: green600, 130 | color: grey50, 131 | cursor: 'default', 132 | }; 133 | const textFieldStyles = { 134 | underlineFocusStyle: { 135 | borderColor: green600, 136 | }, 137 | floatingLabelFocusStyle: { 138 | color: green600, 139 | }, 140 | }; 141 | const datePickerStyles = { 142 | textFieldStyle: { 143 | flex: 1, 144 | }, 145 | }; 146 | const { DateTimeFormat } = global.Intl; 147 | const localeProps = this.props.calendarSystem === 'fa-IR' ? 148 | { utils: persianUtils, DateTimeFormat } : 149 | {}; 150 | const handlers = { 151 | confirmAddNewTaskAndFinish: () => { 152 | !this.buttonDisabled() && this.handleRequestFinish(); 153 | }, 154 | confirmAddNewTaskAndContinue: () => { 155 | !this.buttonDisabled() && this.handleRequestAdd(); 156 | }, 157 | }; 158 | return ( 159 | 167 |
168 |

What do you wanna do?

169 |
170 |
171 | 175 | 184 |
185 | 196 |
197 |
198 | 209 |
210 |
211 | 224 | 228 | 229 | 230 | 231 |
232 |
233 | 245 | 249 | 250 | 251 | 252 |
253 |
254 |
255 |
256 | ); 257 | } 258 | } 259 | 260 | export default NewTaskDialog; 261 | -------------------------------------------------------------------------------- /src/FAB/__tests__/FAB.spec.js: -------------------------------------------------------------------------------- 1 | /* eslint-env mocha, jest */ 2 | 3 | import getActualComponentFactory from '../../lib/testUtils'; 4 | import FAB from '../FAB'; 5 | 6 | const defaultProps = { 7 | calendarSystem: 'en-US', 8 | firstDayOfWeek: 0, 9 | FABRaised: false, 10 | window: null, 11 | width: 1024, 12 | addTask() {}, 13 | addIdea() {}, 14 | }; 15 | const getActualFAB = getActualComponentFactory(FAB, defaultProps); 16 | 17 | it('should render', () => { 18 | getActualFAB(); 19 | }); 20 | it('should be a HotKeys', () => { 21 | const wrapper = getActualFAB(); 22 | expect(wrapper.is('HotKeys')).toBe(true); 23 | }); 24 | it('should have 3 FloatingActionButton', () => { 25 | const wrapper = getActualFAB(); 26 | expect(wrapper.find('FloatingActionButton').length).toBe(3); 27 | }); 28 | it('should have 1 NewTaskDialog', () => { 29 | const wrapper = getActualFAB(); 30 | expect(wrapper.find('NewTaskDialog').length).toBe(1); 31 | }); 32 | it('should have 1 NewIdeaDialog', () => { 33 | const wrapper = getActualFAB(); 34 | expect(wrapper.find('NewIdeaDialog').length).toBe(1); 35 | }); 36 | 37 | it('should set plusFAB bottom style based on props', () => { 38 | const wrapper = getActualFAB({ 39 | FABRaised: true, 40 | width: 767, 41 | }); 42 | expect(wrapper.find('#plus-fab').props().style.bottom).toBe(72); 43 | }); 44 | it('should set doneFAb bottom style based on props', () => { 45 | const wrapper = getActualFAB({ 46 | FABRaised: true, 47 | width: 767, 48 | }); 49 | expect(wrapper.find('#done-fab').props().style.bottom).toBe(80); 50 | }); 51 | it('should set NewTaskDialog calendarSystem based on props', () => { 52 | const wrapper = getActualFAB({ 53 | calendarSystem: 'fa-IR', 54 | }); 55 | expect(wrapper.find('NewTaskDialog').prop('calendarSystem')).toBe('fa-IR'); 56 | }); 57 | it('should set NewTaskDialog firstDayOfWeek based on props', () => { 58 | const wrapper = getActualFAB({ 59 | firstDayOfWeek: 6, 60 | }); 61 | expect(wrapper.find('NewTaskDialog').prop('firstDayOfWeek')).toBe(6); 62 | }); 63 | 64 | it('should call addTask inside NewTaskDialog onRequestAdd', () => { 65 | const wrapper = getActualFAB({ 66 | addTask(task) { 67 | expect(task).toMatchObject({ 68 | task: 'a cool task', 69 | start: 0, 70 | end: 86399999, 71 | estimation: 60, 72 | repetition: 0, 73 | done: false, 74 | }); 75 | }, 76 | }); 77 | wrapper.find('NewTaskDialog').props().onRequestAdd({ 78 | task: 'a cool task', 79 | start: 0, 80 | end: 86399999, 81 | estimation: 1, 82 | estimationValue: 60, 83 | repetition: 0, 84 | repetitionValue: 1, 85 | }); 86 | }); 87 | it('should call addIdea inside NewIdeaDialog onRequestAdd', () => { 88 | const wrapper = getActualFAB({ 89 | addIdea(idea) { 90 | expect(idea).toMatchObject({ 91 | idea: 'a cool idea', 92 | }); 93 | }, 94 | }); 95 | wrapper.find('NewIdeaDialog').props().onRequestAdd({ 96 | idea: 'a cool idea', 97 | }); 98 | }); 99 | 100 | it('should set doneFAB bottom based on state', () => { 101 | const wrapper = getActualFAB(); 102 | wrapper.find('#plus-fab').props().onClick(); 103 | wrapper.update(); 104 | expect(wrapper.find('#done-fab').props().style.bottom).toBe(92); 105 | }); 106 | it('should set lightbulbFAB bottom based on state', () => { 107 | const wrapper = getActualFAB(); 108 | wrapper.find('#plus-fab').props().onClick(); 109 | wrapper.update(); 110 | expect(wrapper.find('#lightbulb-fab').props().style.bottom).toBe(142); 111 | }); 112 | it('should set doneFAB opacity based on state', () => { 113 | const wrapper = getActualFAB(); 114 | wrapper.find('#plus-fab').props().onClick(); 115 | wrapper.update(); 116 | expect(wrapper.find('#done-fab').props().style.opacity).toBe(1); 117 | }); 118 | it('should set lightbulbFAB bottom based on state', () => { 119 | const wrapper = getActualFAB(); 120 | wrapper.find('#plus-fab').props().onClick(); 121 | wrapper.update(); 122 | expect(wrapper.find('#lightbulb-fab').props().style.opacity).toBe(1); 123 | }); 124 | it('should set NewTaskDialog open to true based on state', () => { 125 | const wrapper = getActualFAB(); 126 | wrapper.find('#done-fab').props().onClick(); 127 | wrapper.update(); 128 | expect(wrapper.find('NewTaskDialog').prop('open')).toBe(true); 129 | }); 130 | it('should set NewIdeaDialog open to true based on state', () => { 131 | const wrapper = getActualFAB(); 132 | wrapper.find('#lightbulb-fab').props().onClick(); 133 | wrapper.update(); 134 | expect(wrapper.find('NewIdeaDialog').prop('open')).toBe(true); 135 | }); 136 | it('should set NewTaskDialog open to false based on state', () => { 137 | const wrapper = getActualFAB(); 138 | wrapper.find('NewTaskDialog').props().onRequestClose(); 139 | wrapper.update(); 140 | expect(wrapper.find('NewTaskDialog').prop('open')).toBe(false); 141 | }); 142 | it('should set NewIdeaDialog open to false based on state', () => { 143 | const wrapper = getActualFAB(); 144 | wrapper.find('NewIdeaDialog').props().onRequestClose(); 145 | wrapper.update(); 146 | expect(wrapper.find('NewIdeaDialog').prop('open')).toBe(false); 147 | }); 148 | -------------------------------------------------------------------------------- /src/FAB/__tests__/NewIdeaDialog.spec.js: -------------------------------------------------------------------------------- 1 | /* eslint-env mocha, jest */ 2 | 3 | import getActualComponentFactory from '../../lib/testUtils'; 4 | import NewIdeaDialog from '../NewIdeaDialog'; 5 | 6 | const defaultProps = { 7 | open: false, 8 | onRequestClose() {}, 9 | onRequestAdd() {}, 10 | }; 11 | const getActualDialog = getActualComponentFactory(NewIdeaDialog, defaultProps); 12 | 13 | it('should render', () => { 14 | getActualDialog(); 15 | }); 16 | it('should be a Dialog', () => { 17 | const wrapper = getActualDialog(); 18 | expect(wrapper.is('Dialog')).toBe(true); 19 | }); 20 | it('should have 1 Dialog', () => { 21 | const wrapper = getActualDialog(); 22 | expect(wrapper.find('Dialog').length).toBe(1); 23 | }); 24 | 25 | it('should call onRequestClose inside cancel FlatButton onClick', (done) => { 26 | const wrapper = getActualDialog({ 27 | onRequestClose() { 28 | done(); 29 | }, 30 | }); 31 | wrapper.find('Dialog').prop('actions')[2].props.onClick(); 32 | }); 33 | it('should call onRequestClose inside finish FlatButton onClick', (done) => { 34 | const wrapper = getActualDialog({ 35 | onRequestClose() { 36 | done(); 37 | }, 38 | }); 39 | wrapper.find('Dialog').prop('actions')[0].props.onClick(); 40 | }); 41 | it('should call onRequestAdd inside add FlatButton onClick', (done) => { 42 | const wrapper = getActualDialog({ 43 | onRequestAdd() { 44 | done(); 45 | }, 46 | }); 47 | wrapper.find('Dialog').prop('actions')[1].props.onClick(); 48 | }); 49 | it('should call onRequestAdd inside finish FlatButton onClick', (done) => { 50 | const wrapper = getActualDialog({ 51 | onRequestAdd() { 52 | done(); 53 | }, 54 | }); 55 | wrapper.find('Dialog').prop('actions')[0].props.onClick(); 56 | }); 57 | 58 | it('should set FlatButton disabled based on state', () => { 59 | const wrapper = getActualDialog(); 60 | wrapper.find('TextField').props().onChange({ target: { value: 'a cool idea' } }); 61 | wrapper.update(); 62 | expect(wrapper.find('Dialog').prop('actions')[0].props.disabled).toBe(false); 63 | expect(wrapper.find('Dialog').prop('actions')[1].props.disabled).toBe(false); 64 | }); 65 | it('should set TextField value based on state', () => { 66 | const wrapper = getActualDialog(); 67 | wrapper.find('TextField').props().onChange({ target: { value: 'a cool idea' } }); 68 | wrapper.update(); 69 | expect(wrapper.find('TextField').prop('value')).toBe('a cool idea'); 70 | }); 71 | -------------------------------------------------------------------------------- /src/FAB/__tests__/NewTaskDialog.spec.js: -------------------------------------------------------------------------------- 1 | /* eslint-env mocha, jest */ 2 | 3 | import persianUtils from 'material-ui-persian-date-picker-utils'; 4 | 5 | import getActualComponentFactory from '../../lib/testUtils'; 6 | import NewTaskDialog from '../NewTaskDialog'; 7 | 8 | const defaultProps = { 9 | open: false, 10 | calendarSystem: 'en-US', 11 | onRequestClose() {}, 12 | onRequestAdd() {}, 13 | }; 14 | const getActualDialog = getActualComponentFactory(NewTaskDialog, defaultProps); 15 | 16 | it('should render', () => { 17 | getActualDialog(); 18 | }); 19 | it('should be a Dialog', () => { 20 | const wrapper = getActualDialog(); 21 | expect(wrapper.is('Dialog')).toBe(true); 22 | }); 23 | it('should have 1 Dialog', () => { 24 | const wrapper = getActualDialog(); 25 | expect(wrapper.find('Dialog').length).toBe(1); 26 | }); 27 | 28 | it('should set DatePicker utils based on props', () => { 29 | const wrapper = getActualDialog({ 30 | calendarSystem: 'fa-IR', 31 | }); 32 | expect(wrapper.find('DatePicker').at(0).prop('utils')).toBe(persianUtils); 33 | }); 34 | 35 | it('should call onRequestClose inside cancel FlatButton onClick', (done) => { 36 | const wrapper = getActualDialog({ 37 | onRequestClose() { 38 | done(); 39 | }, 40 | }); 41 | wrapper.find('Dialog').prop('actions')[2].props.onClick(); 42 | }); 43 | it('should call onRequestClose inside finish FlatButton onClick', (done) => { 44 | const wrapper = getActualDialog({ 45 | onRequestClose() { 46 | done(); 47 | }, 48 | }); 49 | wrapper.find('Dialog').prop('actions')[0].props.onClick(); 50 | }); 51 | it('should call onRequestAdd inside add FlatButton onClick', (done) => { 52 | const wrapper = getActualDialog({ 53 | onRequestAdd() { 54 | done(); 55 | }, 56 | }); 57 | wrapper.find('Dialog').prop('actions')[1].props.onClick(); 58 | }); 59 | it('should call onRequestAdd inside finish FlatButton onClick', (done) => { 60 | const wrapper = getActualDialog({ 61 | onRequestAdd() { 62 | done(); 63 | }, 64 | }); 65 | wrapper.find('Dialog').prop('actions')[0].props.onClick(); 66 | }); 67 | 68 | it('should set FlatButton disabled based on state', () => { 69 | const wrapper = getActualDialog(); 70 | const now = new Date(); 71 | wrapper.find('TextField').at(0).props().onChange({ target: { value: 'a cool task' } }); 72 | wrapper.find('TextField').at(1).props().onChange({ target: { value: '10' } }); 73 | wrapper.find('DatePicker').at(0).props().onChange(null, now); 74 | wrapper.find('DatePicker').at(1).props().onChange(null, now); 75 | wrapper.update(); 76 | expect(wrapper.find('Dialog').prop('actions')[0].props.disabled).toBe(false); 77 | expect(wrapper.find('Dialog').prop('actions')[1].props.disabled).toBe(false); 78 | }); 79 | it('should set task TextField value based on state', () => { 80 | const wrapper = getActualDialog(); 81 | wrapper.find('TextField').at(0).props().onChange({ target: { value: 'a cool task' } }); 82 | wrapper.update(); 83 | expect(wrapper.find('TextField').at(0).prop('value')).toBe('a cool task'); 84 | }); 85 | it('should set estimation TextField value based on state', () => { 86 | const wrapper = getActualDialog(); 87 | wrapper.find('TextField').at(1).props().onChange({ target: { value: '10' } }); 88 | wrapper.update(); 89 | expect(wrapper.find('TextField').at(1).prop('value')).toBe('10'); 90 | }); 91 | it('should set estimation TextField errorText based on state', () => { 92 | const wrapper = getActualDialog(); 93 | wrapper.find('TextField').at(1).props().onChange({ target: { value: '10a' } }); 94 | wrapper.update(); 95 | expect(wrapper.find('TextField').at(1).prop('errorText')).not.toBe(''); 96 | }); 97 | it('should set repetition TextField value based on state', () => { 98 | const wrapper = getActualDialog(); 99 | wrapper.find('TextField').at(2).props().onChange({ target: { value: '7' } }); 100 | wrapper.update(); 101 | expect(wrapper.find('TextField').at(2).prop('value')).toBe('7'); 102 | }); 103 | it('should set repetition TextField errorText based on state', () => { 104 | const wrapper = getActualDialog(); 105 | wrapper.find('TextField').at(2).props().onChange({ target: { value: '7a' } }); 106 | wrapper.update(); 107 | expect(wrapper.find('TextField').at(2).prop('errorText')).not.toBe(''); 108 | }); 109 | it('should set estimation DropDownMenu value based on state', () => { 110 | const wrapper = getActualDialog(); 111 | wrapper.find('DropDownMenu').at(0).props().onChange(null, null, '60'); 112 | wrapper.update(); 113 | expect(wrapper.find('DropDownMenu').at(0).prop('value')).toBe('60'); 114 | }); 115 | it('should set repetition DropDownMenu value based on state', () => { 116 | const wrapper = getActualDialog(); 117 | wrapper.find('DropDownMenu').at(1).props().onChange(null, null, '7'); 118 | wrapper.update(); 119 | expect(wrapper.find('DropDownMenu').at(1).prop('value')).toBe('7'); 120 | }); 121 | -------------------------------------------------------------------------------- /src/FAB/actionCreators.js: -------------------------------------------------------------------------------- 1 | const addTask = task => ({ 2 | type: 'ADD_TASK', 3 | task, 4 | }); 5 | const addIdea = idea => ({ 6 | type: 'ADD_IDEA', 7 | idea, 8 | }); 9 | 10 | export { addTask, addIdea }; 11 | -------------------------------------------------------------------------------- /src/FAB/index.js: -------------------------------------------------------------------------------- 1 | export { default } from './FABContainer'; 2 | -------------------------------------------------------------------------------- /src/FAB/lib/__tests__/calculateBottom.spec.js: -------------------------------------------------------------------------------- 1 | /* eslint-env jest */ 2 | import calculateBottom from '../calculateBottom'; 3 | 4 | it('should return 32 when FABOpen is 0', () => { 5 | const actual = 32; 6 | const expected = calculateBottom(0, 0); 7 | expect(actual).toBe(expected); 8 | }); 9 | it('should return 92 if FABOpen is 1 and FABNum is 0', () => { 10 | const actual = 92; 11 | const expected = calculateBottom(1, 0); 12 | expect(actual).toBe(expected); 13 | }); 14 | it('should return 142 if FABOpen is 1 and FABNum is 0', () => { 15 | const actual = 142; 16 | const expected = calculateBottom(1, 1); 17 | expect(actual).toBe(expected); 18 | }); 19 | -------------------------------------------------------------------------------- /src/FAB/lib/calculateBottom.js: -------------------------------------------------------------------------------- 1 | import { 2 | FABMiniBottomClosed, 3 | FABMiniBottomOpen1, 4 | FABMiniBottomOpenDiff, 5 | } from '../../lib/constants'; 6 | 7 | const calculateBottom = (FABOpen, FABNum) => 8 | (FABOpen * (FABMiniBottomOpen1 + (FABNum * FABMiniBottomOpenDiff))) 9 | + (!FABOpen * FABMiniBottomClosed); 10 | 11 | export default calculateBottom; 12 | -------------------------------------------------------------------------------- /src/Help/Help.css: -------------------------------------------------------------------------------- 1 | .Help { 2 | padding-top: 64px; 3 | flex: 1; 4 | transition: margin 200ms cubic-bezier(0.4, 0.0, 0.2, 1) !important; 5 | } 6 | -------------------------------------------------------------------------------- /src/Help/Help.js: -------------------------------------------------------------------------------- 1 | /* eslint-env browser */ 2 | 3 | import React from 'react'; 4 | import { List, ListItem } from 'material-ui/List'; 5 | import Divider from 'material-ui/Divider'; 6 | 7 | import './Help.css'; 8 | 9 | import { 10 | navMiniWidth, 11 | navExpandedWidth, 12 | } from '../lib/constants'; 13 | 14 | const versionURL = 'https://github.com/mkermani144/wanna/releases/tag/Flex-alpha'; 15 | const repoURL = 'https://github.com/mkermani144/wanna'; 16 | const licenseURL = 'https://github.com/mkermani144/wanna/blob/master/LICENSE.md'; 17 | 18 | const Help = ({ sidebarExpanded, openExternal }) => { 19 | // const { shell } = window.require('electron'); 20 | const marginStyles = { 21 | expanded: { 22 | marginLeft: navExpandedWidth, 23 | }, 24 | mini: { 25 | marginLeft: navMiniWidth, 26 | }, 27 | }; 28 | return ( 29 |
37 | 38 | openExternal(versionURL)} 42 | /> 43 | 44 | openExternal(repoURL)} 48 | /> 49 | 50 | openExternal(licenseURL)} 54 | /> 55 | 56 | 57 |
58 | ); 59 | }; 60 | 61 | export default Help; 62 | -------------------------------------------------------------------------------- /src/Help/__tests__/Help.spec.js: -------------------------------------------------------------------------------- 1 | /* eslint-env mocha, jest */ 2 | 3 | import getActualComponentFactory from '../../lib/testUtils'; 4 | import Help from '..'; 5 | 6 | const defaultProps = { 7 | sidebarExpanded: true, 8 | openExternal() {}, 9 | }; 10 | const getActualHelp = getActualComponentFactory(Help, defaultProps); 11 | 12 | it('should render', () => { 13 | getActualHelp(); 14 | }); 15 | it('should be a div', () => { 16 | const wrapper = getActualHelp(); 17 | expect(wrapper.is('div.Help')).toBe(true); 18 | }); 19 | it('should have 1 List', () => { 20 | const wrapper = getActualHelp(); 21 | expect(wrapper.find('List').length).toBe(1); 22 | }); 23 | it('should have 3 ListItem', () => { 24 | const wrapper = getActualHelp(); 25 | expect(wrapper.find('ListItem').length).toBe(3); 26 | }); 27 | it('should have 3 Divider', () => { 28 | const wrapper = getActualHelp(); 29 | expect(wrapper.find('Divider').length).toBe(3); 30 | }); 31 | 32 | it('should set its style based on props', () => { 33 | const wrapper = getActualHelp({ 34 | sidebarExpanded: false, 35 | }); 36 | expect(wrapper.prop('style').marginLeft).toBe(56); 37 | }); 38 | 39 | it('should call openExternal when handling first ListItem onClick', () => { 40 | const wrapper = getActualHelp({ 41 | openExternal(link) { 42 | expect(link).toBe('https://github.com/mkermani144/wanna/releases/tag/Flex-alpha'); 43 | }, 44 | }); 45 | wrapper.find('ListItem').at(0).props().onClick(); 46 | }); 47 | it('should call openExternal when handling second ListItem onClick', () => { 48 | const wrapper = getActualHelp({ 49 | openExternal(link) { 50 | expect(link).toBe('https://github.com/mkermani144/wanna'); 51 | }, 52 | }); 53 | wrapper.find('ListItem').at(1).props().onClick(); 54 | }); 55 | it('should call openExternal when handling third ListItem onClick', () => { 56 | const wrapper = getActualHelp({ 57 | openExternal(link) { 58 | expect(link).toBe('https://github.com/mkermani144/wanna/blob/master/LICENSE.md'); 59 | }, 60 | }); 61 | wrapper.find('ListItem').at(2).props().onClick(); 62 | }); 63 | -------------------------------------------------------------------------------- /src/Help/index.js: -------------------------------------------------------------------------------- 1 | export { default } from './Help'; 2 | -------------------------------------------------------------------------------- /src/Idea/Actions.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import IconButton from 'material-ui/IconButton'; 3 | import Edit from 'material-ui/svg-icons/image/edit'; 4 | import Delete from 'material-ui/svg-icons/action/delete'; 5 | import { green500, grey600, red500 } from 'material-ui/styles/colors'; 6 | 7 | import Fork from '../icons/fork'; 8 | 9 | const Actions = (props) => { 10 | const colors = { 11 | arrow: green500, 12 | edit: grey600, 13 | delete: red500, 14 | }; 15 | return ( 16 |
17 | 22 | 23 | 24 | 29 | 30 | 31 | 36 | 37 | 38 |
39 | ); 40 | }; 41 | 42 | export default Actions; 43 | -------------------------------------------------------------------------------- /src/Idea/Animations.css: -------------------------------------------------------------------------------- 1 | .idea-enter { 2 | opacity: 0; 3 | height: 0; 4 | } 5 | .idea-enter.idea-enter-active { 6 | opacity: 1; 7 | height: 48px; 8 | transition: opacity 110ms cubic-bezier(0.4, 0.0, 0.2, 1) 60ms, 9 | height 170ms cubic-bezier(0.4, 0.0, 0.2, 1); 10 | } 11 | .idea-leave { 12 | opacity: 1; 13 | height: 48px; 14 | } 15 | .idea-leave.idea-leave-active { 16 | opacity: 0; 17 | height: 0px; 18 | transition: opacity 100ms cubic-bezier(0.4, 0.0, 0.2, 1), 19 | height 150ms cubic-bezier(0.4, 0.0, 0.2, 1); 20 | } 21 | 22 | .ideas-empty-state-enter { 23 | opacity: 0; 24 | } 25 | .ideas-empty-state-enter.ideas-empty-state-enter-active { 26 | opacity: 1; 27 | transition: opacity 170ms cubic-bezier(0.0, 0.0, 0.2, 1); 28 | } 29 | .ideas-empty-state-leave { 30 | opacity: 1; 31 | } 32 | .ideas-empty-state-leave.ideas-empty-state-leave-active { 33 | opacity: 0; 34 | transition: opacity 150ms cubic-bezier(0.4, 0.0, 1, 1); 35 | } 36 | -------------------------------------------------------------------------------- /src/Idea/ConvertIdeaDialog.css: -------------------------------------------------------------------------------- 1 | .textfields > * { 2 | display: flex; 3 | flex-flow: row wrap; 4 | align-items: center; 5 | justify-content: space-between; 6 | } 7 | .datepicker { 8 | width: 48%; 9 | display: flex; 10 | } 11 | .datepicker > div { 12 | flex-grow: 1; 13 | display: flex; 14 | } 15 | .datepicker > div > div:first-of-type { 16 | flex-grow: 1; 17 | } 18 | .row { 19 | width: 50%; 20 | display: flex; 21 | flex-direction: row; 22 | align-items: center; 23 | } 24 | .row > div:first-of-type { 25 | width: 60% !important; 26 | } 27 | .row > div:last-of-type { 28 | width: 40% !important; 29 | } 30 | -------------------------------------------------------------------------------- /src/Idea/ConvertIdeaDialog.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import Dialog from 'material-ui/Dialog'; 3 | import FlatButton from 'material-ui/FlatButton'; 4 | import TextField from 'material-ui/TextField'; 5 | import DatePicker from 'material-ui/DatePicker'; 6 | import DropDownMenu from 'material-ui/DropDownMenu'; 7 | import MenuItem from 'material-ui/MenuItem'; 8 | import { green600, grey50 } from 'material-ui/styles/colors'; 9 | import persianUtils from 'material-ui-persian-date-picker-utils'; 10 | import { HotKeys } from 'react-hotkeys'; 11 | 12 | import { 13 | parse, 14 | dayStart, 15 | todayStart, 16 | dayEnd, 17 | } from '../lib/date'; 18 | 19 | import './ConvertIdeaDialog.css'; 20 | 21 | class ConvertIdeaDialog extends Component { 22 | state = { 23 | estimationValue: 1, 24 | repetitionValue: 1, 25 | task: '', 26 | start: todayStart(), 27 | end: null, 28 | estimation: '', 29 | repetition: '', 30 | }; 31 | keyMap = { 32 | confirmConvertIdeaAndFinish: 'shift+enter', 33 | confirmConvertIdeaAndContinue: 'enter', 34 | }; 35 | 36 | buttonDisabled = () => !( 37 | this.state.task 38 | && this.state.end 39 | && this.state.estimation 40 | && /^[0-9]*$/.test(this.state.estimation) 41 | && /^[0-9]*$/.test(this.state.repetition) 42 | ); 43 | handleEstimationMenuChange = (e, i, value) => { 44 | this.setState({ 45 | estimationValue: value, 46 | }); 47 | } 48 | handleRepetitionMenuChange = (e, i, value) => { 49 | this.setState({ 50 | repetitionValue: value, 51 | }); 52 | } 53 | handleTaskChange = (e) => { 54 | this.setState({ 55 | task: e.target.value, 56 | }); 57 | } 58 | handleStartChange = (e, start) => { 59 | this.setState({ 60 | start: dayStart(parse(start)), 61 | }); 62 | } 63 | handleEndChange = (e, end) => { 64 | this.setState({ 65 | end: dayEnd(parse(end)), 66 | }); 67 | } 68 | handleEstimationChange = (e) => { 69 | this.setState({ 70 | estimation: e.target.value, 71 | }); 72 | } 73 | handleRepetitionChange = (e) => { 74 | this.setState({ 75 | repetition: e.target.value, 76 | }); 77 | } 78 | handleRequestClose = () => { 79 | this.setState({ 80 | estimationValue: 1, 81 | repetitionValue: 1, 82 | task: '', 83 | start: todayStart(), 84 | end: null, 85 | estimation: '', 86 | repetition: '', 87 | }); 88 | this.props.onRequestClose(); 89 | } 90 | handleRequestConvert = () => { 91 | this.props.onRequestConvert(this.state); 92 | this.setState({ 93 | estimationValue: 1, 94 | repetitionValue: 1, 95 | task: '', 96 | start: todayStart(), 97 | end: null, 98 | estimation: '', 99 | repetition: '', 100 | }); 101 | } 102 | handleRequestFinish = () => { 103 | this.props.onRequestConvert && this.props.onRequestConvert(this.state); 104 | this.props.onRequestDelete && this.props.onRequestDelete(); 105 | this.props.onRequestClose && this.props.onRequestClose(); 106 | this.setState({ 107 | estimationValue: 1, 108 | repetitionValue: 1, 109 | task: '', 110 | start: todayStart(), 111 | end: null, 112 | estimation: '', 113 | repetition: '', 114 | }); 115 | } 116 | render() { 117 | const actions = [ 118 | , 125 | , 132 | , 137 | ]; 138 | const dialogTitleStyle = { 139 | backgroundColor: green600, 140 | color: grey50, 141 | cursor: 'default', 142 | }; 143 | const textFieldStyles = { 144 | underlineFocusStyle: { 145 | borderColor: green600, 146 | }, 147 | floatingLabelFocusStyle: { 148 | color: green600, 149 | }, 150 | }; 151 | const datePickerStyles = { 152 | textFieldStyle: { 153 | flex: 1, 154 | }, 155 | }; 156 | const { DateTimeFormat } = global.Intl; 157 | const localeProps = this.props.calendarSystem === 'fa-IR' ? 158 | { utils: persianUtils, DateTimeFormat } : 159 | {}; 160 | const handlers = { 161 | confirmConvertIdeaAndFinish: () => { 162 | !this.buttonDisabled() && this.handleRequestFinish(); 163 | }, 164 | confirmConvertIdeaAndContinue: () => { 165 | !this.buttonDisabled() && this.handleRequestAdd(); 166 | }, 167 | }; 168 | return ( 169 | 177 |
178 |

Converting idea: {this.props.idea}

179 |
180 |
181 | 185 | 194 |
195 | 206 |
207 |
208 | 218 |
219 |
220 | 233 | 237 | 238 | 239 | 240 |
241 |
242 | 254 | 258 | 259 | 260 | 261 |
262 |
263 |
264 |
265 | ); 266 | } 267 | } 268 | 269 | export default ConvertIdeaDialog; 270 | -------------------------------------------------------------------------------- /src/Idea/EditIdeaDialog.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import Dialog from 'material-ui/Dialog'; 3 | import FlatButton from 'material-ui/FlatButton'; 4 | import TextField from 'material-ui/TextField'; 5 | import { yellow800, grey50 } from 'material-ui/styles/colors'; 6 | import { HotKeys } from 'react-hotkeys'; 7 | 8 | class EditIdeaDialog extends Component { 9 | state = { idea: this.props.idea }; 10 | keyMap = { confirmEditIdea: 'enter' }; 11 | 12 | handleIdeaChange = (e) => { 13 | this.setState({ 14 | idea: e.target.value, 15 | }); 16 | } 17 | handleRequestClose = () => { 18 | this.props.onRequestClose(); 19 | } 20 | handleRequestEdit = () => { 21 | this.props.onRequestEdit(this.state); 22 | } 23 | render() { 24 | const actions = [ 25 | , 32 | , 37 | ]; 38 | const dialogTitleStyle = { 39 | backgroundColor: yellow800, 40 | color: grey50, 41 | cursor: 'default', 42 | }; 43 | const textFieldStyles = { 44 | underlineFocusStyle: { 45 | borderColor: yellow800, 46 | }, 47 | floatingLabelFocusStyle: { 48 | color: yellow800, 49 | }, 50 | }; 51 | const handlers = { 52 | confirmEditIdea: () => { 53 | this.state.idea && this.handleRequestEdit(); 54 | }, 55 | }; 56 | return ( 57 | 65 |
66 |

Edit you idea

67 |
68 | 72 | 81 | 82 |
83 | ); 84 | } 85 | } 86 | 87 | export default EditIdeaDialog; 88 | -------------------------------------------------------------------------------- /src/Idea/Idea.css: -------------------------------------------------------------------------------- 1 | .Idea { 2 | display: flex; 3 | flex-direction: row; 4 | align-items: center; 5 | min-height: 48px; 6 | padding-left: 16px; 7 | padding-right: 16px; 8 | } 9 | .Idea > * { 10 | flex-basis: 0; 11 | } 12 | .Idea p { 13 | flex-grow: 8; 14 | margin-top: 12px; 15 | margin-bottom: 12px; 16 | overflow: hidden; 17 | white-space: nowrap; 18 | text-overflow: ellipsis; 19 | } 20 | .Idea .Actions { 21 | flex-grow: 2; 22 | display: flex; 23 | flex-direction: row-reverse; 24 | } 25 | .Idea .Actions .IconButton { 26 | opacity: 0; 27 | } 28 | .Idea:hover .Actions .IconButton { 29 | opacity: 1; 30 | } 31 | -------------------------------------------------------------------------------- /src/Idea/Idea.js: -------------------------------------------------------------------------------- 1 | import React, { PureComponent } from 'react'; 2 | 3 | import Actions from './Actions'; 4 | import './Idea.css'; 5 | 6 | class Idea extends PureComponent { 7 | state = { class: '' }; 8 | 9 | handleRequestDelete = () => { 10 | this.props.onRequestDelete && this.props.onRequestDelete(this.props.index); 11 | this.props.onRequestSnackbar && this.props.onRequestSnackbar('Idea deleted'); 12 | } 13 | render() { 14 | return ( 15 |
16 |

{this.props.text}

17 | this.props.onRequestEditDialogOpen(this.props.index)} 19 | onRequestDelete={this.handleRequestDelete} 20 | onRequestConvertDialogOpen={() => this.props.onRequestConvertDialogOpen(this.props.index)} 21 | /> 22 |
23 | ); 24 | } 25 | } 26 | 27 | export default Idea; 28 | -------------------------------------------------------------------------------- /src/Idea/IdeaList.css: -------------------------------------------------------------------------------- 1 | .IdeaList { 2 | padding-top: 64px; 3 | flex: 1; 4 | display: flex; 5 | flex-direction: column; 6 | justify-content: flex-start; 7 | transition: margin 200ms cubic-bezier(0.4, 0.0, 0.2, 1) !important; 8 | } 9 | .transition-container { 10 | position: relative; 11 | } 12 | .ideas-empty-state { 13 | position: absolute; 14 | top: 50vh; 15 | left: 50%; 16 | transform: translate(-50%, -50%); 17 | flex: 1; 18 | background-color: #FAFAFA; 19 | color: #90A4AE; 20 | display: flex; 21 | flex-direction: column; 22 | justify-content: center; 23 | align-items: center; 24 | cursor: default; 25 | user-select: none; 26 | } 27 | .ideas-empty-state h1 { 28 | font-size: 500%; 29 | margin-bottom: 0; 30 | opacity: .2; 31 | } 32 | .ideas-empty-state h4 { 33 | font-size: 200%; 34 | margin-top: 0; 35 | opacity: .25; 36 | } 37 | -------------------------------------------------------------------------------- /src/Idea/IdeaList.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import Divider from 'material-ui/Divider'; 3 | import Snackbar from 'material-ui/Snackbar'; 4 | import shortid from 'shortid'; 5 | import CSSTransitionGroup from 'react-addons-css-transition-group'; 6 | 7 | import Idea from './Idea'; 8 | import EditIdeaDialog from './EditIdeaDialog'; 9 | import ConvertIdeaDialog from './ConvertIdeaDialog'; 10 | import './IdeaList.css'; 11 | import './Animations.css'; 12 | 13 | import { 14 | navMiniWidth, 15 | navExpandedWidth, 16 | transitionEnterTimeout, 17 | transitionLeaveTimeout, 18 | } from '../lib/constants'; 19 | 20 | class IdeaList extends Component { 21 | state = { 22 | ideaDialogOpen: false, 23 | convertDialogOpen: false, 24 | snackbarOpen: false, 25 | snackbarMessage: '', 26 | index: -1, 27 | current: 5, 28 | }; 29 | 30 | componentDidMount = () => { 31 | this.interval = setInterval(() => this.renderMore(), 0); 32 | } 33 | componentWillUnmount = () => { 34 | clearInterval(this.interval); 35 | } 36 | interval = null; 37 | handleRequestIdeaDialogClose = () => { 38 | this.setState({ 39 | ideaDialogOpen: false, 40 | }); 41 | } 42 | handleRequestIdeaDialogOpen = (index) => { 43 | this.setState({ 44 | ideaDialogOpen: true, 45 | index, 46 | }); 47 | } 48 | handleRequestIdeaEdit = (ideaInfo) => { 49 | this.props.editIdea(this.state.index, { 50 | idea: ideaInfo.idea, 51 | }); 52 | this.handleRequestIdeaDialogClose(); 53 | } 54 | handleRequestIdeaDelete = (index) => { 55 | this.props.deleteIdea(index); 56 | } 57 | handleRequestConvertDialogClose = () => { 58 | this.setState({ 59 | convertDialogOpen: false, 60 | }); 61 | } 62 | handleRequestConvertDialogDelete = () => { 63 | this.props.deleteIdea(this.state.index); 64 | } 65 | handleRequestConvertDialogOpen = (index) => { 66 | this.setState({ 67 | convertDialogOpen: true, 68 | index, 69 | }); 70 | } 71 | handleRequestIdeaConvert = (taskInfo) => { 72 | const repetitionDays = taskInfo.repetition * taskInfo.repetitionValue; 73 | const id = shortid.generate(); 74 | this.props.addTask({ 75 | task: taskInfo.task, 76 | start: taskInfo.start, 77 | end: taskInfo.end, 78 | estimation: taskInfo.estimation * taskInfo.estimationValue, 79 | repetition: repetitionDays, 80 | done: false, 81 | id, 82 | }); 83 | } 84 | handleRequestSnackbarOpen = (message) => { 85 | this.setState({ 86 | snackbarOpen: true, 87 | snackbarMessage: message, 88 | }); 89 | this.props.raiseFab(); 90 | } 91 | handleRequestSnackbarClose = () => { 92 | this.setState({ 93 | snackbarOpen: false, 94 | snackbarMessage: '', 95 | }); 96 | this.props.lowerFab(); 97 | } 98 | handleUndo = () => { 99 | this.props.undo(); 100 | this.handleRequestSnackbarClose(); 101 | } 102 | renderMore = () => { 103 | if (this.state.current === this.props.ideas.length - 1) { 104 | clearInterval(this.interval); 105 | } else { 106 | this.setState(prev => ({ current: prev.current + 1 })); 107 | } 108 | } 109 | render() { 110 | const marginStyles = { 111 | expanded: { 112 | marginLeft: navExpandedWidth, 113 | }, 114 | mini: { 115 | marginLeft: navMiniWidth, 116 | }, 117 | }; 118 | return ( 119 |
128 | 134 | {!this.props.ideas.length && 135 |
138 |

139 | Ideas gone 140 |

141 |

142 | Your ideas list is empty 143 |

144 |
145 | } 146 |
147 | 152 | {this.props.ideas.map((idea, index) => (index > this.state.current ? 153 |
: 154 | ( 155 |
156 | 164 | 165 |
166 | ))) 167 | } 168 | 169 | 178 | 190 | 198 |
199 | ); 200 | } 201 | } 202 | 203 | export default IdeaList; 204 | -------------------------------------------------------------------------------- /src/Idea/IdeaListContainer.js: -------------------------------------------------------------------------------- 1 | import { connect } from 'react-redux'; 2 | import { bindActionCreators } from 'redux'; 3 | 4 | import * as actionCreators from './actionCreators'; 5 | import IdeaList from './IdeaList'; 6 | 7 | const mapStateToProps = state => ({ 8 | ideas: state.ideas.present, 9 | calendarSystem: state.appProperties.calendarSystem, 10 | firstDayOfWeek: state.appProperties.firstDayOfWeek, 11 | }); 12 | 13 | const mapDispatchToProps = dispatch => bindActionCreators(actionCreators, dispatch); 14 | 15 | const IdeaListContainer = connect(mapStateToProps, mapDispatchToProps)(IdeaList); 16 | 17 | export default IdeaListContainer; 18 | -------------------------------------------------------------------------------- /src/Idea/__tests__/Actions.spec.js: -------------------------------------------------------------------------------- 1 | /* eslint-env mocha, jest */ 2 | 3 | import getActualComponentFactory from '../../lib/testUtils'; 4 | import Actions from '../Actions'; 5 | 6 | const getActualActions = getActualComponentFactory(Actions, {}); 7 | 8 | it('should render', () => { 9 | getActualActions(); 10 | }); 11 | it('should have 3 IconButton', () => { 12 | const wrapper = getActualActions(); 13 | expect(wrapper.find('IconButton').length).toBe(3); 14 | }); 15 | -------------------------------------------------------------------------------- /src/Idea/__tests__/ConvertIdeaDialog.spec.js: -------------------------------------------------------------------------------- 1 | /* eslint-env mocha, jest */ 2 | 3 | import getActualComponentFactory from '../../lib/testUtils'; 4 | import ConvertIdeaDialog from '../ConvertIdeaDialog'; 5 | 6 | const defaultProps = { 7 | open: false, 8 | calendarSystem: 'en-US', 9 | firstDayOfWeek: 0, 10 | onRequestClose() {}, 11 | onRequestConvert() {}, 12 | onRequestDelete() {}, 13 | }; 14 | const getActualDialog = getActualComponentFactory(ConvertIdeaDialog, defaultProps); 15 | 16 | it('should render', () => { 17 | getActualDialog(); 18 | }); 19 | it('should be a Dialog', () => { 20 | const wrapper = getActualDialog(); 21 | expect(wrapper.is('Dialog')).toBe(true); 22 | }); 23 | it('should have 1 Dialog', () => { 24 | const wrapper = getActualDialog(); 25 | expect(wrapper.find('Dialog').length).toBe(1); 26 | }); 27 | it('should set Dialog open based on props', () => { 28 | const wrapper = getActualDialog({ 29 | open: true, 30 | }); 31 | expect(wrapper.find('Dialog').prop('open')).toBe(true); 32 | }); 33 | it('should set DatePicker locale based on props', () => { 34 | const wrapper = getActualDialog({ 35 | calendarSystem: 'fa-IR', 36 | }); 37 | expect(wrapper.find('DatePicker').at(0).prop('locale')).toBe('fa-IR'); 38 | }); 39 | it('should set DatePicker firstDayOfWeek based on props', () => { 40 | const wrapper = getActualDialog({ 41 | firstDayOfWeek: 6, 42 | }); 43 | expect(wrapper.find('DatePicker').at(0).prop('firstDayOfWeek')).toBe(6); 44 | }); 45 | 46 | it('should call onRequestClose inside cancel FlatButton onClick', (done) => { 47 | const wrapper = getActualDialog({ 48 | onRequestClose() { 49 | done(); 50 | }, 51 | }); 52 | wrapper.find('Dialog').prop('actions')[2].props.onClick(); 53 | }); 54 | it('should call onRequestConvert inside convert FlatButton onClick', (done) => { 55 | const wrapper = getActualDialog({ 56 | onRequestConvert() { 57 | done(); 58 | }, 59 | }); 60 | wrapper.find('Dialog').prop('actions')[1].props.onClick(); 61 | }); 62 | it('should call onRequestDelete inside finish FlatButton onClick', (done) => { 63 | const wrapper = getActualDialog({ 64 | onRequestDelete() { 65 | done(); 66 | }, 67 | }); 68 | wrapper.find('Dialog').prop('actions')[0].props.onClick(); 69 | }); 70 | 71 | it('should set FlatButton disabled based on state', () => { 72 | const wrapper = getActualDialog(); 73 | const now = new Date(); 74 | wrapper.find('TextField').at(0).props().onChange({ target: { value: 'a cool task' } }); 75 | wrapper.find('TextField').at(1).props().onChange({ target: { value: '10' } }); 76 | wrapper.find('DatePicker').at(0).props().onChange(null, now); 77 | wrapper.find('DatePicker').at(1).props().onChange(null, now); 78 | wrapper.update(); 79 | expect(wrapper.find('Dialog').prop('actions')[0].props.disabled).toBe(false); 80 | expect(wrapper.find('Dialog').prop('actions')[1].props.disabled).toBe(false); 81 | }); 82 | it('should set task TextField value based on state', () => { 83 | const wrapper = getActualDialog(); 84 | wrapper.find('TextField').at(0).props().onChange({ target: { value: 'a cool task' } }); 85 | wrapper.update(); 86 | expect(wrapper.find('TextField').at(0).prop('value')).toBe('a cool task'); 87 | }); 88 | it('should set estimation TextField value based on state', () => { 89 | const wrapper = getActualDialog(); 90 | wrapper.find('TextField').at(1).props().onChange({ target: { value: '10' } }); 91 | wrapper.update(); 92 | expect(wrapper.find('TextField').at(1).prop('value')).toBe('10'); 93 | }); 94 | it('should set estimation TextField errorText based on state', () => { 95 | const wrapper = getActualDialog(); 96 | wrapper.find('TextField').at(1).props().onChange({ target: { value: '10a' } }); 97 | wrapper.update(); 98 | expect(wrapper.find('TextField').at(1).prop('errorText')).not.toBe(''); 99 | }); 100 | it('should set repetition TextField value based on state', () => { 101 | const wrapper = getActualDialog(); 102 | wrapper.find('TextField').at(2).props().onChange({ target: { value: '7' } }); 103 | wrapper.update(); 104 | expect(wrapper.find('TextField').at(2).prop('value')).toBe('7'); 105 | }); 106 | it('should set repetition TextField errorText based on state', () => { 107 | const wrapper = getActualDialog(); 108 | wrapper.find('TextField').at(2).props().onChange({ target: { value: '7a' } }); 109 | wrapper.update(); 110 | expect(wrapper.find('TextField').at(2).prop('errorText')).not.toBe(''); 111 | }); 112 | it('should set estimation DropDownMenu value based on state', () => { 113 | const wrapper = getActualDialog(); 114 | wrapper.find('DropDownMenu').at(0).props().onChange(null, null, '60'); 115 | wrapper.update(); 116 | expect(wrapper.find('DropDownMenu').at(0).prop('value')).toBe('60'); 117 | }); 118 | it('should set repetition DropDownMenu value based on state', () => { 119 | const wrapper = getActualDialog(); 120 | wrapper.find('DropDownMenu').at(1).props().onChange(null, null, '7'); 121 | wrapper.update(); 122 | expect(wrapper.find('DropDownMenu').at(1).prop('value')).toBe('7'); 123 | }); 124 | -------------------------------------------------------------------------------- /src/Idea/__tests__/EditIdeaDialog.spec.js: -------------------------------------------------------------------------------- 1 | /* eslint-env mocha, jest */ 2 | 3 | import getActualComponentFactory from '../../lib/testUtils'; 4 | import EditIdeaDialog from '../EditIdeaDialog'; 5 | 6 | const defaultProps = { 7 | open: false, 8 | idea: '', 9 | onRequestClose() {}, 10 | onRequestEdit() {}, 11 | }; 12 | const getActualDialog = getActualComponentFactory(EditIdeaDialog, defaultProps); 13 | 14 | it('should render', () => { 15 | getActualDialog(); 16 | }); 17 | it('should be a Dialog', () => { 18 | const wrapper = getActualDialog(); 19 | expect(wrapper.is('Dialog')).toBe(true); 20 | }); 21 | it('should have 1 Dialog', () => { 22 | const wrapper = getActualDialog(); 23 | expect(wrapper.find('Dialog').length).toBe(1); 24 | }); 25 | it('should set Dialog open based on props', () => { 26 | const wrapper = getActualDialog({ 27 | open: true, 28 | }); 29 | expect(wrapper.find('Dialog').prop('open')).toBe(true); 30 | }); 31 | it('should set TextField defaultValue based on props', () => { 32 | const wrapper = getActualDialog({ 33 | idea: 'a cool idea', 34 | }); 35 | expect(wrapper.find('TextField').prop('defaultValue')).toBe('a cool idea'); 36 | }); 37 | it('should call onRequestClose inside cancel FlatButton onClick', (done) => { 38 | const wrapper = getActualDialog({ 39 | onRequestClose() { 40 | done(); 41 | }, 42 | }); 43 | wrapper.find('Dialog').prop('actions')[1].props.onClick(); 44 | }); 45 | it('should call onRequestEdit inside edit FlatButton onClick', (done) => { 46 | const wrapper = getActualDialog({ 47 | onRequestEdit() { 48 | done(); 49 | }, 50 | }); 51 | wrapper.find('Dialog').prop('actions')[0].props.onClick(); 52 | }); 53 | 54 | it('should set FlatButton disabled based on state', () => { 55 | const wrapper = getActualDialog(); 56 | wrapper.find('TextField').props().onChange({ target: { value: 'a cool idea' } }); 57 | wrapper.update(); 58 | expect(wrapper.find('Dialog').prop('actions')[0].props.disabled).toBe(false); 59 | }); 60 | -------------------------------------------------------------------------------- /src/Idea/__tests__/Idea.spec.js: -------------------------------------------------------------------------------- 1 | /* eslint-env mocha, jest */ 2 | 3 | import getActualComponentFactory from '../../lib/testUtils'; 4 | import Idea from '../Idea'; 5 | 6 | const defaultProps = { 7 | index: 0, 8 | onRequestEditDialogOpen() {}, 9 | onRequestConvertDialogOpen() {}, 10 | onRequestDelete() {}, 11 | }; 12 | const getActualIdea = getActualComponentFactory(Idea, defaultProps); 13 | 14 | jest.useFakeTimers(); 15 | 16 | it('should render', () => { 17 | getActualIdea(); 18 | }); 19 | it('should be a div', () => { 20 | const wrapper = getActualIdea(); 21 | expect(wrapper.is('div.Idea')).toBe(true); 22 | }); 23 | it('should have 1 p', () => { 24 | const wrapper = getActualIdea(); 25 | expect(wrapper.find('p').length).toBe(1); 26 | }); 27 | it('should have 1 Actions', () => { 28 | const wrapper = getActualIdea(); 29 | expect(wrapper.find('Actions').length).toBe(1); 30 | }); 31 | it('should set actions onRequestEditDialogOpen based on props', (done) => { 32 | const wrapper = getActualIdea({ 33 | onRequestEditDialogOpen() { 34 | done(); 35 | }, 36 | }); 37 | wrapper.find('Actions').props().onRequestEditDialogOpen(); 38 | }); 39 | it('should set actions onRequestConvertDialogOpen based on props', (done) => { 40 | const wrapper = getActualIdea({ 41 | onRequestConvertDialogOpen() { 42 | done(); 43 | }, 44 | }); 45 | wrapper.find('Actions').props().onRequestConvertDialogOpen(); 46 | }); 47 | it('should call onRequestEditDialogOpen inside Actions onRequestEditDialogOpen', () => { 48 | const wrapper = getActualIdea({ 49 | index: 5, 50 | onRequestEditDialogOpen(index) { 51 | expect(index).toBe(5); 52 | }, 53 | }); 54 | wrapper.find('Actions').props().onRequestEditDialogOpen(); 55 | }); 56 | it('should call onRequestConvertDialogOpen inside Actions onRequestConvertDialogOpen', () => { 57 | const wrapper = getActualIdea({ 58 | index: 5, 59 | onRequestConvertDialogOpen(index) { 60 | expect(index).toBe(5); 61 | }, 62 | }); 63 | wrapper.find('Actions').props().onRequestConvertDialogOpen(); 64 | }); 65 | it('should call onRequestDelete inside Actions onRequestDelete', (done) => { 66 | const wrapper = getActualIdea({ 67 | onRequestDelete() { 68 | done(); 69 | }, 70 | }); 71 | wrapper.find('Actions').props().onRequestDelete(); 72 | jest.runAllTimers(); 73 | }); 74 | -------------------------------------------------------------------------------- /src/Idea/__tests__/IdeaList.spec.js: -------------------------------------------------------------------------------- 1 | /* eslint-env mocha, jest */ 2 | 3 | import getActualComponentFactory from '../../lib/testUtils'; 4 | import IdeaList from '../IdeaList'; 5 | 6 | const defaultProps = { 7 | ideas: [ 8 | { 9 | idea: 'a cool idea', 10 | }, 11 | { 12 | idea: 'another cool idea', 13 | }, 14 | ], 15 | sidebarExpanded: true, 16 | calendarSystem: 'en-US', 17 | firstDayOfWeek: 0, 18 | editIdea() {}, 19 | deleteIdea() {}, 20 | addTask() {}, 21 | raiseFab() {}, 22 | lowerFab() {}, 23 | undo() {}, 24 | }; 25 | const getActualIdeaList = getActualComponentFactory(IdeaList, defaultProps); 26 | 27 | 28 | it('should render', () => { 29 | getActualIdeaList(); 30 | }); 31 | it('should be a div', () => { 32 | const wrapper = getActualIdeaList(); 33 | expect(wrapper.is('div.IdeaList')).toBe(true); 34 | }); 35 | it('should have 2 CSSTransitionGroup', () => { 36 | const wrapper = getActualIdeaList(); 37 | expect(wrapper.find('CSSTransitionGroup').length).toBe(2); 38 | }); 39 | it('should have 2 Idea', () => { 40 | const wrapper = getActualIdeaList(); 41 | expect(wrapper.find('Idea').length).toBe(2); 42 | }); 43 | it('should have 2 Divider', () => { 44 | const wrapper = getActualIdeaList(); 45 | expect(wrapper.find('Divider').length).toBe(2); 46 | }); 47 | it('should have 2 EditIdeaDialog', () => { 48 | const wrapper = getActualIdeaList(); 49 | expect(wrapper.find('EditIdeaDialog').length).toBe(1); 50 | }); 51 | it('should have 2 ConvertIdeaDialog', () => { 52 | const wrapper = getActualIdeaList(); 53 | expect(wrapper.find('ConvertIdeaDialog').length).toBe(1); 54 | }); 55 | it('should have 1 Snackbar', () => { 56 | const wrapper = getActualIdeaList(); 57 | expect(wrapper.find('Snackbar').length).toBe(1); 58 | }); 59 | 60 | it('should have 1 ideas-empty-state if ideas is empty', () => { 61 | const wrapper = getActualIdeaList({ 62 | ideas: [], 63 | }); 64 | expect(wrapper.find('.ideas-empty-state').length).toBe(1); 65 | }); 66 | it('should have 0 Idea if ideas is empty', () => { 67 | const wrapper = getActualIdeaList({ 68 | ideas: [], 69 | }); 70 | expect(wrapper.find('Idea').length).toBe(0); 71 | }); 72 | it('should have 0 Divider if ideas is empty', () => { 73 | const wrapper = getActualIdeaList({ 74 | ideas: [], 75 | }); 76 | expect(wrapper.find('Divider').length).toBe(0); 77 | }); 78 | 79 | it('should set left margin style based on props', () => { 80 | const wrapper = getActualIdeaList({ 81 | sidebarExpanded: false, 82 | }); 83 | expect(wrapper.prop('style').marginLeft).toBe(56); 84 | }); 85 | it('should set left margin style based on props if idea is empty', () => { 86 | const wrapper = getActualIdeaList({ 87 | sidebarExpanded: false, 88 | ideas: [], 89 | }); 90 | expect(wrapper.prop('style').marginLeft).toBe(56); 91 | }); 92 | it('should set ConvertIdeaDialog calendarSystem based on props', () => { 93 | const wrapper = getActualIdeaList({ 94 | calendarSystem: 'fa-IR', 95 | }); 96 | expect(wrapper.find('ConvertIdeaDialog').prop('calendarSystem')).toBe('fa-IR'); 97 | }); 98 | it('should set ConvertIdeaDialog firstDayOfWeek based on props', () => { 99 | const wrapper = getActualIdeaList({ 100 | firstDayOfWeek: 6, 101 | }); 102 | expect(wrapper.find('ConvertIdeaDialog').prop('firstDayOfWeek')).toBe(6); 103 | }); 104 | 105 | it('should call editIdea inside EditIdeaDialog onRequestEdit', () => { 106 | const wrapper = getActualIdeaList({ 107 | editIdea(index, ideaInfo) { 108 | expect(index).toBe(2); 109 | expect(ideaInfo.idea).toBe('a cool idea'); 110 | }, 111 | }); 112 | wrapper.find('Idea').at(0).props().onRequestEditDialogOpen(2); 113 | wrapper.find('EditIdeaDialog').props().onRequestEdit({ idea: 'a cool idea' }); 114 | }); 115 | it('should call deleteIdea inside Idea onRequestDelete', () => { 116 | const wrapper = getActualIdeaList({ 117 | deleteIdea(index) { 118 | expect(index).toBe(3); 119 | }, 120 | }); 121 | wrapper.find('Idea').at(0).props().onRequestDelete(3); 122 | }); 123 | it('should call deleteIdea inside ConvertIdeaDialog onRequestDelete', () => { 124 | const wrapper = getActualIdeaList({ 125 | deleteIdea(index) { 126 | expect(index).toBe(3); 127 | }, 128 | }); 129 | wrapper.find('Idea').at(0).props().onRequestConvertDialogOpen(3); 130 | wrapper.find('ConvertIdeaDialog').props().onRequestDelete(3); 131 | }); 132 | it('should call addTask inside ConvertIdeaDialog onRequestConvert', () => { 133 | const wrapper = getActualIdeaList({ 134 | addTask(taskInfo) { 135 | expect(taskInfo.done).toBe(false); 136 | expect(taskInfo.start).toBe(0); 137 | expect(taskInfo.end).toBe(86399999); 138 | expect(taskInfo.estimation).toBe(120); 139 | expect(taskInfo.repetition).toBe(0); 140 | expect(taskInfo.task).toBe('a cool task'); 141 | }, 142 | }); 143 | wrapper.find('ConvertIdeaDialog').props().onRequestConvert({ 144 | start: 0, 145 | end: 86399999, 146 | estimation: 2, 147 | estimationValue: 60, 148 | repetition: 0, 149 | repetitionValue: 1, 150 | task: 'a cool task', 151 | }); 152 | }); 153 | it('should call raiseFab inside Idea onRequestSnackbar', (done) => { 154 | const wrapper = getActualIdeaList({ 155 | raiseFab() { 156 | done(); 157 | }, 158 | }); 159 | wrapper.find('Idea').at(0).props().onRequestSnackbar(); 160 | }); 161 | it('should call lowerFab inside Snackbar onRequestClose', (done) => { 162 | const wrapper = getActualIdeaList({ 163 | lowerFab() { 164 | done(); 165 | }, 166 | }); 167 | wrapper.find('Snackbar').props().onRequestClose(); 168 | }); 169 | it('should call undo inside Snackbar onActionClick', (done) => { 170 | const wrapper = getActualIdeaList({ 171 | undo() { 172 | done(); 173 | }, 174 | }); 175 | wrapper.find('Snackbar').props().onActionClick(); 176 | }); 177 | 178 | it('should set EditIdeaDialog open based on state', () => { 179 | const wrapper = getActualIdeaList(); 180 | wrapper.find('Idea').at(0).props().onRequestEditDialogOpen(0); 181 | wrapper.update(); 182 | expect(wrapper.find('EditIdeaDialog').prop('open')).toBe(true); 183 | }); 184 | it('should set EditIdeaDialog idea based on state', () => { 185 | const wrapper = getActualIdeaList(); 186 | wrapper.find('Idea').at(0).props().onRequestEditDialogOpen(0); 187 | wrapper.update(); 188 | expect(wrapper.find('EditIdeaDialog').prop('idea')).toBe('a cool idea'); 189 | }); 190 | it('should set ConvertIdeaDialog open to true based on state', () => { 191 | const wrapper = getActualIdeaList(); 192 | wrapper.find('Idea').at(0).props().onRequestConvertDialogOpen(0); 193 | wrapper.update(); 194 | expect(wrapper.find('ConvertIdeaDialog').prop('open')).toBe(true); 195 | }); 196 | it('should set ConvertIdeaDialog open to false based on state', () => { 197 | const wrapper = getActualIdeaList(); 198 | wrapper.find('ConvertIdeaDialog').props().onRequestClose(); 199 | wrapper.update(); 200 | expect(wrapper.find('ConvertIdeaDialog').prop('open')).toBe(false); 201 | }); 202 | it('should set ConvertIdeaDialog idea based on state', () => { 203 | const wrapper = getActualIdeaList(); 204 | wrapper.find('Idea').at(0).props().onRequestEditDialogOpen(0); 205 | wrapper.update(); 206 | expect(wrapper.find('ConvertIdeaDialog').prop('idea')).toBe('a cool idea'); 207 | }); 208 | it('should set Snackbar open based on state', () => { 209 | const wrapper = getActualIdeaList(); 210 | wrapper.find('Idea').at(0).props().onRequestSnackbar(); 211 | wrapper.update(); 212 | expect(wrapper.find('Snackbar').prop('open')).toBe(true); 213 | }); 214 | it('should set Snackbar message based on state', () => { 215 | const wrapper = getActualIdeaList(); 216 | wrapper.find('Idea').at(0).props().onRequestSnackbar('a cool message'); 217 | wrapper.update(); 218 | expect(wrapper.find('Snackbar').prop('message')).toBe('a cool message'); 219 | }); 220 | -------------------------------------------------------------------------------- /src/Idea/actionCreators.js: -------------------------------------------------------------------------------- 1 | import { ActionCreators as UndoActionCreators } from 'redux-undo'; 2 | 3 | const editIdea = (index, newIdea) => ({ 4 | type: 'EDIT_IDEA', 5 | index, 6 | newIdea, 7 | }); 8 | const deleteIdea = index => ({ 9 | type: 'DELETE_IDEA', 10 | index, 11 | }); 12 | const addTask = task => ({ 13 | type: 'ADD_TASK', 14 | task, 15 | }); 16 | const raiseFab = () => ({ 17 | type: 'RAISE_FAB', 18 | }); 19 | const lowerFab = () => ({ 20 | type: 'LOWER_FAB', 21 | }); 22 | const { undo } = UndoActionCreators; 23 | 24 | export { editIdea, deleteIdea, addTask, raiseFab, lowerFab, undo }; 25 | -------------------------------------------------------------------------------- /src/Idea/index.js: -------------------------------------------------------------------------------- 1 | export { default } from './IdeaListContainer'; 2 | -------------------------------------------------------------------------------- /src/Settings/CalendarSystemDialog.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import Dialog from 'material-ui/Dialog'; 3 | import { RadioButton, RadioButtonGroup } from 'material-ui/RadioButton'; 4 | import FlatButton from 'material-ui/FlatButton'; 5 | import { blue500, grey50 } from 'material-ui/styles/colors'; 6 | 7 | class CalendarSystemDialog extends Component { 8 | handleRequestClose = (e, target) => { 9 | setTimeout(() => { 10 | this.props.onRequestClose(target); 11 | }, 300); 12 | } 13 | render() { 14 | const actions = [ 15 | this.props.onRequestClose(this.props.calendarSystem)} 19 | />, 20 | ]; 21 | const dialogContentStyle = { 22 | maxWidth: 256, 23 | }; 24 | const radioButtonStyle = { 25 | marginTop: 16, 26 | }; 27 | const dialogTitleStyle = { 28 | backgroundColor: blue500, 29 | color: grey50, 30 | cursor: 'default', 31 | }; 32 | return ( 33 | this.props.onRequestClose(this.props.calendarSystem)} 40 | > 41 | 46 | 51 | 56 | 57 | 58 | ); 59 | } 60 | } 61 | 62 | export default CalendarSystemDialog; 63 | -------------------------------------------------------------------------------- /src/Settings/FirstDayOfWeekDialog.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import Dialog from 'material-ui/Dialog'; 3 | import { RadioButton, RadioButtonGroup } from 'material-ui/RadioButton'; 4 | import FlatButton from 'material-ui/FlatButton'; 5 | import { blue500, grey50 } from 'material-ui/styles/colors'; 6 | 7 | class FirstDayOfWeekDialog extends Component { 8 | handleRequestClose = (e, target) => { 9 | setTimeout(() => { 10 | this.props.onRequestClose(target); 11 | }, 300); 12 | } 13 | render() { 14 | const actions = [ 15 | this.props.onRequestClose(this.props.firstDayOfWeek)} 19 | />, 20 | ]; 21 | const dialogContentStyle = { 22 | maxWidth: 256, 23 | }; 24 | const radioButtonStyle = { 25 | marginTop: 16, 26 | }; 27 | const dialogTitleStyle = { 28 | backgroundColor: blue500, 29 | color: grey50, 30 | cursor: 'default', 31 | }; 32 | return ( 33 | this.props.onRequestClose(this.props.firstDayOfWeek)} 40 | > 41 | 46 | 51 | 56 | 61 | 62 | 63 | ); 64 | } 65 | } 66 | 67 | export default FirstDayOfWeekDialog; 68 | -------------------------------------------------------------------------------- /src/Settings/Settings.css: -------------------------------------------------------------------------------- 1 | .Settings { 2 | padding-top: 64px; 3 | flex: 1; 4 | transition: margin 200ms cubic-bezier(0.4, 0.0, 0.2, 1) !important; 5 | } 6 | -------------------------------------------------------------------------------- /src/Settings/Settings.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { List, ListItem } from 'material-ui/List'; 3 | import Checkbox from 'material-ui/Checkbox'; 4 | import Divider from 'material-ui/Divider'; 5 | 6 | import CalendarSystemDialog from './CalendarSystemDialog'; 7 | import FirstDayOfWeekDialog from './FirstDayOfWeekDialog'; 8 | import StartupTabDialog from './StartupTabDialog'; 9 | import './Settings.css'; 10 | 11 | import { 12 | navMiniWidth, 13 | navExpandedWidth, 14 | } from '../lib/constants'; 15 | 16 | class Settings extends Component { 17 | state = { 18 | calendarSystemDialogOpen: false, 19 | firstDayOfWeekDialogOpen: false, 20 | startupTabDialogOpen: false, 21 | }; 22 | 23 | handleCheckShowNotYet = (e, checked) => { 24 | this.props.toggleNotYet(checked); 25 | } 26 | handleCheckFullscreen = (e, checked) => { 27 | this.props.toggleFullscreen(checked); 28 | } 29 | handleRequestCalendarSystemDialogOpen = () => { 30 | this.setState({ 31 | calendarSystemDialogOpen: true, 32 | }); 33 | } 34 | handleRequestFirstDayOfWeekDialogOpen = () => { 35 | this.setState({ 36 | firstDayOfWeekDialogOpen: true, 37 | }); 38 | } 39 | handleRequestStartupTabDialogOpen = () => { 40 | this.setState({ 41 | startupTabDialogOpen: true, 42 | }); 43 | } 44 | handleRequestCalendarSystemDialogClose = (calendarSystem) => { 45 | this.props.changeCalendarSystem(calendarSystem); 46 | this.setState({ 47 | calendarSystemDialogOpen: false, 48 | }); 49 | } 50 | handleRequestFirstDayOfWeekDialogClose = (dayNumber) => { 51 | this.props.changeFirstDayOfWeek(+dayNumber); 52 | this.setState({ 53 | firstDayOfWeekDialogOpen: false, 54 | }); 55 | } 56 | handleRequestStartupTabDialogClose = (startupTab) => { 57 | this.props.changeStartupTab(startupTab); 58 | this.setState({ 59 | startupTabDialogOpen: false, 60 | }); 61 | } 62 | render() { 63 | const weekDays = { 64 | 0: 'Sunday', 65 | 1: 'Monday', 66 | 6: 'Saturday', 67 | }; 68 | const marginStyles = { 69 | expanded: { 70 | marginLeft: navExpandedWidth, 71 | }, 72 | mini: { 73 | marginLeft: navMiniWidth, 74 | }, 75 | }; 76 | return ( 77 |
85 | 86 | 91 | 92 | 97 | 98 | 103 | 104 | 110 | } 111 | primaryText="Fullscreen mode" 112 | secondaryText="Start the app in full width and height. Changes will apply after restarting the app" 113 | /> 114 | 115 | 122 | } 123 | primaryText="Not-yet tasks" 124 | secondaryText="Show the section in task list" 125 | /> 126 | 127 | 128 | 133 | 138 | 143 |
144 | ); 145 | } 146 | } 147 | 148 | export default Settings; 149 | -------------------------------------------------------------------------------- /src/Settings/SettingsContainer.js: -------------------------------------------------------------------------------- 1 | import { connect } from 'react-redux'; 2 | import { bindActionCreators } from 'redux'; 3 | 4 | import * as actionCreators from './actionCreators'; 5 | import Settings from './Settings'; 6 | 7 | const mapStateToProps = state => ({ 8 | fullscreen: state.appProperties.fullscreen, 9 | showNotYetTasks: state.appProperties.showNotYetTasks, 10 | calendarSystem: state.appProperties.calendarSystem, 11 | firstDayOfWeek: state.appProperties.firstDayOfWeek, 12 | startupTab: state.appProperties.startupTab, 13 | }); 14 | 15 | const mapDispatchToProps = dispatch => bindActionCreators(actionCreators, dispatch); 16 | 17 | const SettingsContainer = connect(mapStateToProps, mapDispatchToProps)(Settings); 18 | 19 | export default SettingsContainer; 20 | -------------------------------------------------------------------------------- /src/Settings/StartupTabDialog.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import Dialog from 'material-ui/Dialog'; 3 | import { RadioButton, RadioButtonGroup } from 'material-ui/RadioButton'; 4 | import FlatButton from 'material-ui/FlatButton'; 5 | import { blue500, grey50 } from 'material-ui/styles/colors'; 6 | 7 | class StartupTabDialog extends Component { 8 | handleRequestClose = (e, target) => { 9 | setTimeout(() => { 10 | this.props.onRequestClose(target); 11 | }, 300); 12 | } 13 | render() { 14 | const actions = [ 15 | this.props.onRequestClose(this.props.startupTab)} 19 | />, 20 | ]; 21 | const dialogContentStyle = { 22 | maxWidth: 256, 23 | }; 24 | const radioButtonStyle = { 25 | marginTop: 16, 26 | }; 27 | const dialogTitleStyle = { 28 | backgroundColor: blue500, 29 | color: grey50, 30 | }; 31 | return ( 32 | this.props.onRequestClose(this.props.startupTab)} 39 | > 40 | 45 | 50 | 55 | 56 | 57 | ); 58 | } 59 | } 60 | 61 | export default StartupTabDialog; 62 | -------------------------------------------------------------------------------- /src/Settings/__tests__/CalendarSystemDialog.spec.js: -------------------------------------------------------------------------------- 1 | /* eslint-env mocha, jest */ 2 | 3 | import getActualComponentFactory from '../../lib/testUtils'; 4 | import CalendarSystemDialog from '../CalendarSystemDialog'; 5 | 6 | const defaultProps = { 7 | open: false, 8 | calendarSystem: 'en-US', 9 | onRequestClose() {}, 10 | }; 11 | const getActualDialog = getActualComponentFactory(CalendarSystemDialog, defaultProps); 12 | 13 | jest.useFakeTimers(); 14 | 15 | it('should render', () => { 16 | getActualDialog(); 17 | }); 18 | it('should be a Dialog', () => { 19 | const wrapper = getActualDialog(); 20 | expect(wrapper.is('Dialog')).toBe(true); 21 | }); 22 | it('should have 2 RadioButton', () => { 23 | const wrapper = getActualDialog(); 24 | expect(wrapper.find('RadioButton').length).toBe(2); 25 | }); 26 | it('should set Dialog open based on props', () => { 27 | const wrapper = getActualDialog({ open: true }); 28 | expect(wrapper.find('Dialog').prop('open')).toBe(true); 29 | }); 30 | it('should set RadioButtonGroup defaultSelected based on props', () => { 31 | const wrapper = getActualDialog({ calendarSystem: 'fa-IR' }); 32 | expect(wrapper.find('RadioButtonGroup').prop('defaultSelected')).toBe('fa-IR'); 33 | }); 34 | it('should call onRequestClose inside Dialog onRequestClose', () => { 35 | const wrapper = getActualDialog({ 36 | calendarSystem: 'fa-IR', 37 | onRequestClose(calendarSystem) { 38 | expect(calendarSystem).toBe('fa-IR'); 39 | }, 40 | }); 41 | wrapper.find('Dialog').props().onRequestClose(); 42 | }); 43 | it('should call onRequestClose inside FlatButton onClick', () => { 44 | const wrapper = getActualDialog({ 45 | calendarSystem: 'fa-IR', 46 | onRequestClose(calendarSystem) { 47 | expect(calendarSystem).toBe('fa-IR'); 48 | }, 49 | }); 50 | wrapper.find('Dialog').prop('actions')[0].props.onClick(); 51 | }); 52 | it('should call onRequestClose inside RadioButtonGroup onChange', () => { 53 | const wrapper = getActualDialog({ 54 | calendarSystem: 'fa-IR', 55 | onRequestClose(calendarSystem) { 56 | expect(calendarSystem).toBe('fa-IR'); 57 | }, 58 | }); 59 | wrapper.find('RadioButtonGroup').props().onChange(null, 'fa-IR'); 60 | jest.runAllTimers(); 61 | }); 62 | -------------------------------------------------------------------------------- /src/Settings/__tests__/FirstDayOfWeekDialog.spec.js: -------------------------------------------------------------------------------- 1 | /* eslint-env mocha, jest */ 2 | 3 | import getActualComponentFactory from '../../lib/testUtils'; 4 | import FirstDayOfWeekDialog from '../FirstDayOfWeekDialog'; 5 | 6 | const defaultProps = { 7 | open: false, 8 | firstDayOfWeek: 0, 9 | onRequestClose() {}, 10 | }; 11 | const getActualDialog = getActualComponentFactory(FirstDayOfWeekDialog, defaultProps); 12 | 13 | jest.useFakeTimers(); 14 | 15 | it('should render', () => { 16 | getActualDialog(); 17 | }); 18 | it('should be a Dialog', () => { 19 | const wrapper = getActualDialog(); 20 | expect(wrapper.is('Dialog')).toBe(true); 21 | }); 22 | it('should have 3 RadioButton', () => { 23 | const wrapper = getActualDialog(); 24 | expect(wrapper.find('RadioButton').length).toBe(3); 25 | }); 26 | it('should set dialog open based on props', () => { 27 | const wrapper = getActualDialog({ open: true }); 28 | expect(wrapper.find('Dialog').prop('open')).toBe(true); 29 | }); 30 | it('should set RadioButtonGroup defaultSelected based on props', () => { 31 | const wrapper = getActualDialog({ firstDayOfWeek: 6 }); 32 | expect(wrapper.find('RadioButtonGroup').prop('defaultSelected')).toBe('6'); 33 | }); 34 | it('should call onRequestClose inside Dialog onRequestClose', () => { 35 | const wrapper = getActualDialog({ 36 | firstDayOfWeek: 6, 37 | onRequestClose(firstDayOfWeek) { 38 | expect(firstDayOfWeek).toBe(6); 39 | }, 40 | }); 41 | wrapper.find('Dialog').props().onRequestClose(); 42 | }); 43 | it('should call onRequestClose inside FlatButton onClick', () => { 44 | const wrapper = getActualDialog({ 45 | firstDayOfWeek: 6, 46 | onRequestClose(firstDayOfWeek) { 47 | expect(firstDayOfWeek).toBe(6); 48 | }, 49 | }); 50 | wrapper.find('Dialog').prop('actions')[0].props.onClick(); 51 | }); 52 | it('should call onRequestClose inside RadioButtonGroup onChange', () => { 53 | const wrapper = getActualDialog({ 54 | firstDayOfWeek: 6, 55 | onRequestClose(firstDayOfWeek) { 56 | expect(firstDayOfWeek).toBe(6); 57 | }, 58 | }); 59 | wrapper.find('RadioButtonGroup').props().onChange(null, 6); 60 | jest.runAllTimers(); 61 | }); 62 | -------------------------------------------------------------------------------- /src/Settings/__tests__/Settings.spec.js: -------------------------------------------------------------------------------- 1 | /* eslint-env mocha, jest */ 2 | 3 | import getActualComponentFactory from '../../lib/testUtils'; 4 | import Settings from '../Settings'; 5 | 6 | const defaultProps = { 7 | sidebarExpanded: true, 8 | calendarSystem: 'en-US', 9 | firstDayOfWeek: 0, 10 | startupTab: 'tasks', 11 | fullscreen: false, 12 | showNotYetTasks: true, 13 | toggleNotYet() {}, 14 | toggleFullscreen() {}, 15 | changeCalendarSystem() {}, 16 | changeFirstDayOfWeek() {}, 17 | changeStartupTab() {}, 18 | }; 19 | const getActualSettings = getActualComponentFactory(Settings, defaultProps); 20 | 21 | 22 | jest.useFakeTimers(); 23 | 24 | it('should render', () => { 25 | getActualSettings(); 26 | }); 27 | it('should be a div', () => { 28 | const wrapper = getActualSettings(); 29 | expect(wrapper.is('div.Settings')).toBe(true); 30 | }); 31 | it('should have 5 ListItem', () => { 32 | const wrapper = getActualSettings(); 33 | expect(wrapper.find('ListItem').length).toBe(5); 34 | }); 35 | it('should have 5 Divider', () => { 36 | const wrapper = getActualSettings(); 37 | expect(wrapper.find('Divider').length).toBe(5); 38 | }); 39 | it('should have 1 CalendarSystemDialog', () => { 40 | const wrapper = getActualSettings(); 41 | expect(wrapper.find('CalendarSystemDialog').length).toBe(1); 42 | }); 43 | it('should have 1 FirstDayOfWeekDialog', () => { 44 | const wrapper = getActualSettings(); 45 | expect(wrapper.find('FirstDayOfWeekDialog').length).toBe(1); 46 | }); 47 | it('should have 1 StartupTabDialog', () => { 48 | const wrapper = getActualSettings(); 49 | expect(wrapper.find('StartupTabDialog').length).toBe(1); 50 | }); 51 | 52 | it('should set left margin style based on props', () => { 53 | const wrapper = getActualSettings({ 54 | sidebarExpanded: false, 55 | }); 56 | expect(wrapper.prop('style').marginLeft).toBe(56); 57 | }); 58 | it('should set ListItem secondaryText based on props', () => { 59 | const wrapper = getActualSettings({ 60 | calendarSystem: 'fa-IR', 61 | }); 62 | expect(wrapper.find('ListItem').at(0).prop('secondaryText')).toBe('fa-IR'); 63 | }); 64 | it('should set fullscreen Checkbox defaultChecked based on props', () => { 65 | const wrapper = getActualSettings({ 66 | fullscreen: true, 67 | }); 68 | expect(wrapper.find('ListItem').at(3).prop('leftCheckbox').props.defaultChecked).toBe(true); 69 | }); 70 | it('should set show not yet tasks Checkbox defaultChecked based on props', () => { 71 | const wrapper = getActualSettings({ 72 | showNotYetTasks: false, 73 | }); 74 | expect(wrapper.find('ListItem').at(4).prop('leftCheckbox').props.defaultChecked).toBe(false); 75 | }); 76 | it('should set CalendarSystemDialog calendarSystem based on props', () => { 77 | const wrapper = getActualSettings({ 78 | calendarSystem: 'fa-IR', 79 | }); 80 | expect(wrapper.find('CalendarSystemDialog').prop('calendarSystem')).toBe('fa-IR'); 81 | }); 82 | it('should set FirstDayOfWeekDialog dialog firstDayOfWeek based on props', () => { 83 | const wrapper = getActualSettings({ 84 | firstDayOfWeek: 6, 85 | }); 86 | expect(wrapper.find('FirstDayOfWeekDialog').prop('firstDayOfWeek')).toBe(6); 87 | }); 88 | it('should set StartupTabDialog dialog startupTab based on props', () => { 89 | const wrapper = getActualSettings({ 90 | startupTab: 'ideas', 91 | }); 92 | expect(wrapper.find('StartupTabDialog').prop('startupTab')).toBe('ideas'); 93 | }); 94 | 95 | it('should call toggleFullscreen when handling fullscreen check', () => { 96 | const wrapper = getActualSettings({ 97 | toggleFullscreen(checked) { 98 | expect(checked).toBe(true); 99 | }, 100 | }); 101 | wrapper.find('ListItem').at(3).prop('leftCheckbox').props.onCheck(null, true); 102 | }); 103 | it('should call toggleNotYet when handling show not yet check', () => { 104 | const wrapper = getActualSettings({ 105 | toggleNotYet(checked) { 106 | expect(checked).toBe(true); 107 | }, 108 | }); 109 | wrapper.find('ListItem').at(4).prop('leftCheckbox').props.onCheck(null, true); 110 | }); 111 | it('should call changeCalendarSystem when handling calendar system dialog close request', () => { 112 | const wrapper = getActualSettings({ 113 | changeCalendarSystem(calendarSystem) { 114 | expect(calendarSystem).toBe('fa-IR'); 115 | }, 116 | }); 117 | wrapper.find('CalendarSystemDialog').props().onRequestClose('fa-IR'); 118 | }); 119 | it('should call changeFirstDayOfWeek when handling first day of week dialog close request', () => { 120 | const wrapper = getActualSettings({ 121 | changeFirstDayOfWeek(firstDayOfWeek) { 122 | expect(firstDayOfWeek).toBe(6); 123 | }, 124 | }); 125 | wrapper.find('FirstDayOfWeekDialog').props().onRequestClose(6); 126 | }); 127 | it('should call changeStartupTab when handling startup tab dialog close request', () => { 128 | const wrapper = getActualSettings({ 129 | changeStartupTab(startupTab) { 130 | expect(startupTab).toBe('ideas'); 131 | }, 132 | }); 133 | wrapper.find('StartupTabDialog').props().onRequestClose('ideas'); 134 | }); 135 | 136 | it('should set CalendarSystemDialog open based on state', () => { 137 | const wrapper = getActualSettings(); 138 | wrapper.find('ListItem').at(0).props().onClick(); 139 | wrapper.update(); 140 | expect(wrapper.find('CalendarSystemDialog').prop('open')).toBe(true); 141 | }); 142 | it('should set FirstDayOfWeekDialog open based on state', () => { 143 | const wrapper = getActualSettings(); 144 | wrapper.find('ListItem').at(1).props().onClick(); 145 | wrapper.update(); 146 | expect(wrapper.find('FirstDayOfWeekDialog').prop('open')).toBe(true); 147 | }); 148 | it('should set StartupTabDialog open based on state', () => { 149 | const wrapper = getActualSettings(); 150 | wrapper.find('ListItem').at(2).props().onClick(); 151 | wrapper.update(); 152 | expect(wrapper.find('StartupTabDialog').prop('open')).toBe(true); 153 | }); 154 | -------------------------------------------------------------------------------- /src/Settings/__tests__/StartupTabDialog.spec.js: -------------------------------------------------------------------------------- 1 | /* eslint-env mocha, jest */ 2 | 3 | import getActualComponentFactory from '../../lib/testUtils'; 4 | import StartupTabDialog from '../StartupTabDialog'; 5 | 6 | const defaultProps = { 7 | open: false, 8 | startupTab: 'tasks', 9 | onRequestClose() {}, 10 | }; 11 | const getActualDialog = getActualComponentFactory(StartupTabDialog, defaultProps); 12 | 13 | jest.useFakeTimers(); 14 | 15 | it('should render', () => { 16 | getActualDialog(); 17 | }); 18 | it('should be a Dialog', () => { 19 | const wrapper = getActualDialog(); 20 | expect(wrapper.is('Dialog')).toBe(true); 21 | }); 22 | it('should have 2 RadioButton', () => { 23 | const wrapper = getActualDialog(); 24 | expect(wrapper.find('RadioButton').length).toBe(2); 25 | }); 26 | it('should set Dialog open based on props', () => { 27 | const wrapper = getActualDialog({ open: true }); 28 | expect(wrapper.find('Dialog').prop('open')).toBe(true); 29 | }); 30 | it('should set RadioButtonGroup defaultSelected based on props', () => { 31 | const wrapper = getActualDialog({ startupTab: 'ideas' }); 32 | expect(wrapper.find('RadioButtonGroup').prop('defaultSelected')).toBe('ideas'); 33 | }); 34 | it('should call onRequestClose inside Dialog onRequestClose', () => { 35 | const wrapper = getActualDialog({ 36 | startupTab: 'ideas', 37 | onRequestClose(startupTab) { 38 | expect(startupTab).toBe('ideas'); 39 | }, 40 | }); 41 | wrapper.find('Dialog').props().onRequestClose(); 42 | }); 43 | it('should call onRequestClose inside FlatButton onClick', () => { 44 | const wrapper = getActualDialog({ 45 | startupTab: 'ideas', 46 | onRequestClose(startupTab) { 47 | expect(startupTab).toBe('ideas'); 48 | }, 49 | }); 50 | wrapper.find('Dialog').prop('actions')[0].props.onClick(); 51 | }); 52 | it('should call onRequestClose inside RadioButtonGroup onChange', () => { 53 | const wrapper = getActualDialog({ 54 | startupTab: 'ideas', 55 | onRequestClose(startupTab) { 56 | expect(startupTab).toBe('ideas'); 57 | }, 58 | }); 59 | wrapper.find('RadioButtonGroup').props().onChange(null, 'ideas'); 60 | jest.runAllTimers(); 61 | }); 62 | -------------------------------------------------------------------------------- /src/Settings/actionCreators.js: -------------------------------------------------------------------------------- 1 | const toggleNotYet = flag => ({ 2 | type: 'TOGGLE_NOT_YET', 3 | flag, 4 | }); 5 | const toggleFullscreen = isFullscreen => ({ 6 | type: 'TOGGLE_FULLSCREEN', 7 | isFullscreen, 8 | }); 9 | const changeCalendarSystem = calendarSystem => ({ 10 | type: 'CHANGE_CALENDAR_SYSTEM', 11 | calendarSystem, 12 | }); 13 | const changeFirstDayOfWeek = firstDayOfWeek => ({ 14 | type: 'CHANGE_FIRST_DAY_OF_WEEK', 15 | firstDayOfWeek, 16 | }); 17 | const changeStartupTab = startupTab => ({ 18 | type: 'CHANGE_STARTUP_TAB', 19 | startupTab, 20 | }); 21 | 22 | export { 23 | toggleNotYet, 24 | toggleFullscreen, 25 | changeCalendarSystem, 26 | changeFirstDayOfWeek, 27 | changeStartupTab, 28 | }; 29 | -------------------------------------------------------------------------------- /src/Settings/index.js: -------------------------------------------------------------------------------- 1 | export { default } from './SettingsContainer'; 2 | -------------------------------------------------------------------------------- /src/Sidebar/Sidebar.css: -------------------------------------------------------------------------------- 1 | .Sidebar { 2 | display: flex; 3 | flex-direction: column; 4 | flex-shrink: 1; 5 | position: fixed; 6 | left: 0; 7 | top: 0; 8 | z-index: 10; 9 | } 10 | .Sidebar > div { 11 | padding-top: 64px; 12 | overflow-x: hidden !important; 13 | transition: width 200ms cubic-bezier(0.4, 0.0, 0.2, 1) !important; 14 | } 15 | -------------------------------------------------------------------------------- /src/Sidebar/Sidebar.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Link } from 'react-router-dom'; 3 | import { List, ListItem } from 'material-ui/List'; 4 | import Drawer from 'material-ui/Drawer'; 5 | import DoneAll from 'material-ui/svg-icons/action/done-all'; 6 | import LightbulbOutline from 'material-ui/svg-icons/action/lightbulb-outline'; 7 | import Settings from 'material-ui/svg-icons/action/settings'; 8 | import Help from 'material-ui/svg-icons/action/help'; 9 | import { 10 | green600, 11 | green800, 12 | yellow800, 13 | yellow900, 14 | indigo600, 15 | indigo900, 16 | cyan600, 17 | cyan700, 18 | } from 'material-ui/styles/colors'; 19 | 20 | import './Sidebar.css'; 21 | 22 | import { 23 | navMiniWidth, 24 | navExpandedWidth, 25 | } from '../lib/constants'; 26 | 27 | const Sidebar = (props) => { 28 | const styles = { 29 | tasks: { 30 | color: green800, 31 | }, 32 | ideas: { 33 | color: yellow900, 34 | }, 35 | settings: { 36 | color: indigo900, 37 | }, 38 | help: { 39 | color: cyan700, 40 | }, 41 | }; 42 | return ( 43 | 49 | 50 | 61 | } 62 | style={ 63 | props.currentTab === 'tasks' ? 64 | styles.tasks : 65 | null 66 | } 67 | containerElement={} 68 | onClick={() => props.changeTab('tasks')} 69 | /> 70 | 81 | } 82 | style={ 83 | props.currentTab === 'ideas' ? 84 | styles.ideas : 85 | null 86 | } 87 | containerElement={} 88 | onClick={() => props.changeTab('ideas')} 89 | /> 90 | 101 | } 102 | style={ 103 | props.currentTab === 'settings' ? 104 | styles.settings : 105 | null 106 | } 107 | containerElement={} 108 | onClick={() => props.changeTab('settings')} 109 | /> 110 | 121 | } 122 | style={ 123 | props.currentTab === 'help' ? 124 | styles.help : 125 | null 126 | } 127 | containerElement={} 128 | onClick={() => props.changeTab('help')} 129 | /> 130 | 131 | 132 | ); 133 | }; 134 | 135 | export default Sidebar; 136 | -------------------------------------------------------------------------------- /src/Sidebar/SidebarContainer.js: -------------------------------------------------------------------------------- 1 | import { connect } from 'react-redux'; 2 | import { bindActionCreators } from 'redux'; 3 | 4 | import changeTab from './actionCreators'; 5 | import Sidebar from './Sidebar'; 6 | 7 | const mapStateToProps = state => ({ 8 | currentTab: state.appUI.currentTab, 9 | startupTab: state.appProperties.startupTab, 10 | }); 11 | 12 | const mapDispatchToProps = dispatch => bindActionCreators({ changeTab }, dispatch); 13 | 14 | const SidebarContainer = connect(mapStateToProps, mapDispatchToProps)(Sidebar); 15 | 16 | export default SidebarContainer; 17 | -------------------------------------------------------------------------------- /src/Sidebar/__test__/Sidebar.spec.js: -------------------------------------------------------------------------------- 1 | /* eslint-env mocha, jest */ 2 | 3 | import getActualComponentFactory from '../../lib/testUtils'; 4 | import Sidebar from '../Sidebar'; 5 | 6 | const defaultProps = { 7 | expanded: true, 8 | currentTab: 'tasks', 9 | changeTab() {}, 10 | }; 11 | const getActualSidebar = getActualComponentFactory(Sidebar, defaultProps); 12 | 13 | it('should render', () => { 14 | getActualSidebar(); 15 | }); 16 | it('should be a Drawer', () => { 17 | const wrapper = getActualSidebar(); 18 | expect(wrapper.is('Drawer')).toBe(true); 19 | }); 20 | it('should have 4 ListItem', () => { 21 | const wrapper = getActualSidebar(); 22 | expect(wrapper.find('ListItem').length).toBe(4); 23 | }); 24 | 25 | it('should set its width based on props', () => { 26 | const wrapper = getActualSidebar({ 27 | expanded: false, 28 | }); 29 | expect(wrapper.props().width).toBe(56); 30 | }); 31 | it('should highlight ideas tab if current tab is ideas', () => { 32 | const wrapper = getActualSidebar({ 33 | currentTab: 'ideas', 34 | }); 35 | expect(wrapper.find('ListItem').at(1).prop('style')).not.toBeNull(); 36 | }); 37 | it('should highlight settings tab if current tab is settings', () => { 38 | const wrapper = getActualSidebar({ 39 | currentTab: 'settings', 40 | }); 41 | expect(wrapper.find('ListItem').at(2).prop('style')).not.toBeNull(); 42 | }); 43 | it('should highlight help tab if current tab is help', () => { 44 | const wrapper = getActualSidebar({ 45 | currentTab: 'help', 46 | }); 47 | expect(wrapper.find('ListItem').at(3).prop('style')).not.toBeNull(); 48 | }); 49 | 50 | it('should call changeTab inside tasks ListItem onClick', () => { 51 | const wrapper = getActualSidebar({ 52 | changeTab(tab) { 53 | expect(tab).toBe('tasks'); 54 | }, 55 | }); 56 | wrapper.find('ListItem').at(0).props().onClick(); 57 | }); 58 | it('should call changeTab inside ideas ListItem onClick', () => { 59 | const wrapper = getActualSidebar({ 60 | changeTab(tab) { 61 | expect(tab).toBe('ideas'); 62 | }, 63 | }); 64 | wrapper.find('ListItem').at(1).props().onClick(); 65 | }); 66 | it('should call changeTab inside settings ListItem onClick', () => { 67 | const wrapper = getActualSidebar({ 68 | changeTab(tab) { 69 | expect(tab).toBe('settings'); 70 | }, 71 | }); 72 | wrapper.find('ListItem').at(2).props().onClick(); 73 | }); 74 | it('should call changeTab inside help ListItem onClick', () => { 75 | const wrapper = getActualSidebar({ 76 | changeTab(tab) { 77 | expect(tab).toBe('help'); 78 | }, 79 | }); 80 | wrapper.find('ListItem').at(3).props().onClick(); 81 | }); 82 | -------------------------------------------------------------------------------- /src/Sidebar/actionCreators.js: -------------------------------------------------------------------------------- 1 | const changeTab = tab => ({ 2 | type: 'CHANGE_TAB', 3 | tab, 4 | }); 5 | 6 | export default changeTab; 7 | -------------------------------------------------------------------------------- /src/Sidebar/index.js: -------------------------------------------------------------------------------- 1 | export { default } from './SidebarContainer'; 2 | -------------------------------------------------------------------------------- /src/Task/Actions.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import IconButton from 'material-ui/IconButton'; 3 | import Done from 'material-ui/svg-icons/action/done'; 4 | import Edit from 'material-ui/svg-icons/image/edit'; 5 | import Delete from 'material-ui/svg-icons/action/delete'; 6 | import { blue500, grey600, red500 } from 'material-ui/styles/colors'; 7 | 8 | const Actions = (props) => { 9 | const colors = { 10 | done: blue500, 11 | edit: grey600, 12 | delete: red500, 13 | }; 14 | return ( 15 |
16 | 21 | 22 | 23 | {!props.done && 24 | 29 | 30 | 31 | } 32 | {!props.done && 33 | 38 | 39 | 40 | } 41 |
42 | ); 43 | }; 44 | 45 | export default Actions; 46 | -------------------------------------------------------------------------------- /src/Task/Animations.css: -------------------------------------------------------------------------------- 1 | .task-enter { 2 | opacity: 0; 3 | height: 0; 4 | } 5 | .task-enter.task-enter-active { 6 | opacity: 1; 7 | height: 48px; 8 | transition: opacity 110ms cubic-bezier(0.4, 0.0, 0.2, 1) 60ms, 9 | height 170ms cubic-bezier(0.4, 0.0, 0.2, 1); 10 | } 11 | .task-leave { 12 | opacity: 1; 13 | height: 48px; 14 | } 15 | .task-leave.task-leave-active { 16 | opacity: 0; 17 | height: 0px; 18 | transition: opacity 100ms cubic-bezier(0.4, 0.0, 0.2, 1), 19 | height 150ms cubic-bezier(0.4, 0.0, 0.2, 1); 20 | } 21 | 22 | .tasks-empty-state-enter, 23 | .task-header-enter, 24 | .task-divider-enter { 25 | opacity: 0; 26 | } 27 | .tasks-empty-state-enter.tasks-empty-state-enter-active, 28 | .task-header-enter.task-header-enter-active, 29 | .task-divider-enter.task-divider-enter-active { 30 | opacity: 1; 31 | transition: opacity 170ms cubic-bezier(0.0, 0.0, 0.2, 1); 32 | } 33 | .tasks-empty-state-leave, 34 | .task-header-leave, 35 | .task-divider-leave { 36 | opacity: 1; 37 | } 38 | .tasks-empty-state-leave.tasks-empty-state-leave-active, 39 | .task-header-leave.task-header-leave-active, 40 | .task-divider-leave.task-divider-leave-active { 41 | opacity: 0; 42 | transition: opacity 150ms cubic-bezier(0.4, 0.0, 1, 1); 43 | } 44 | -------------------------------------------------------------------------------- /src/Task/Circle.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const Circle = ({ color, signal }) => ( 4 |
5 | {signal &&
} 6 |
7 | ); 8 | 9 | export default Circle; 10 | -------------------------------------------------------------------------------- /src/Task/DueDate.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Timer from 'material-ui/svg-icons/image/timer'; 3 | import { amber800, red500 } from 'material-ui/styles/colors'; 4 | 5 | const DueDate = ({ due }) => { 6 | const colors = { 7 | tomorrow: amber800, 8 | today: red500, 9 | }; 10 | const styles = { 11 | tomorrow: { 12 | color: amber800, 13 | }, 14 | today: { 15 | color: red500, 16 | }, 17 | icon: { 18 | width: 16, 19 | height: 16, 20 | }, 21 | }; 22 | switch (due) { 23 | case 'tomorrow': 24 | return ( 25 | 26 | 27 | 31 | Tomorrow 32 | 33 | 34 | ); 35 | case 'today': 36 | return ( 37 | 38 | 39 | 43 | Today 44 | 45 | 46 | ); 47 | default: 48 | return ( 49 | 50 | 51 | 52 | ); 53 | } 54 | }; 55 | 56 | export default DueDate; 57 | -------------------------------------------------------------------------------- /src/Task/EditTaskDialog.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import Dialog from 'material-ui/Dialog'; 3 | import FlatButton from 'material-ui/FlatButton'; 4 | import TextField from 'material-ui/TextField'; 5 | import { green600, grey50 } from 'material-ui/styles/colors'; 6 | import { HotKeys } from 'react-hotkeys'; 7 | 8 | class EditTaskDialog extends Component { 9 | state = { task: this.props.task }; 10 | keyMap = { confirmEditTask: 'enter' }; 11 | 12 | handleTaskChange = (e) => { 13 | this.setState({ 14 | task: e.target.value, 15 | }); 16 | } 17 | handleRequestClose = () => { 18 | this.props.onRequestClose(); 19 | } 20 | handleRequestEdit = () => { 21 | this.props.onRequestEdit(this.state); 22 | } 23 | render() { 24 | const actions = [ 25 | , 32 | , 37 | ]; 38 | const dialogTitleStyle = { 39 | backgroundColor: green600, 40 | color: grey50, 41 | cursor: 'default', 42 | }; 43 | const textFieldStyles = { 44 | underlineFocusStyle: { 45 | borderColor: green600, 46 | }, 47 | floatingLabelFocusStyle: { 48 | color: green600, 49 | }, 50 | }; 51 | const handlers = { 52 | confirmEditTask: () => { 53 | this.state.task && this.handleRequestEdit(); 54 | }, 55 | }; 56 | return ( 57 | 65 |
66 |

Edit your task

67 |
68 | 72 | 81 | 82 |
83 | ); 84 | } 85 | } 86 | 87 | export default EditTaskDialog; 88 | -------------------------------------------------------------------------------- /src/Task/Estimation.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { blue700 } from 'material-ui/styles/colors'; 3 | 4 | const Estimation = ({ estimation }) => { 5 | const style = { 6 | color: blue700, 7 | }; 8 | return ( 9 | {estimation} min 10 | ); 11 | }; 12 | 13 | export default Estimation; 14 | -------------------------------------------------------------------------------- /src/Task/Repeat.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import RepeatIcon from 'material-ui/svg-icons/av/repeat'; 3 | import { grey500 } from 'material-ui/styles/colors'; 4 | 5 | const Repeat = ({ repeat }) => { 6 | const styles = { 7 | text: { 8 | color: grey500, 9 | }, 10 | icon: { 11 | width: 16, 12 | height: 16, 13 | }, 14 | }; 15 | return ( 16 | 17 | { 18 | repeat && repeat.indexOf('0') !== 0 ? 19 | 20 | : 21 | null 22 | } 23 | { 24 | repeat && repeat.indexOf('0') ? 25 | repeat : 26 | null 27 | } 28 | 29 | ); 30 | }; 31 | 32 | export default Repeat; 33 | -------------------------------------------------------------------------------- /src/Task/Task.css: -------------------------------------------------------------------------------- 1 | @keyframes signal { 2 | to { 3 | width: 16px; 4 | height: 16px; 5 | opacity: 0; 6 | border-width: 2px; 7 | } 8 | } 9 | 10 | .Task { 11 | display: flex; 12 | flex-direction: row; 13 | align-items: center; 14 | height: 48px; 15 | padding-left: 16px; 16 | padding-right: 16px; 17 | } 18 | .Task.done .text p { 19 | color: #9E9E9E; 20 | transition-duration: 1s; 21 | } 22 | .Task .text p:before { 23 | content: ''; 24 | position: absolute; 25 | width: 0%; 26 | height: 1px; 27 | top: 55%; 28 | margin-top: -0.5px; 29 | background: #424242; 30 | } 31 | .Task.done .text p:before { 32 | width: 100%; 33 | } 34 | .Task > * { 35 | flex-basis: 0; 36 | } 37 | .Task p { 38 | margin-top: 12px; 39 | margin-bottom: 12px; 40 | } 41 | .Task .Circle { 42 | flex-grow: 0; 43 | margin-right: 10px; 44 | position: relative; 45 | width: 8px; 46 | height: 8px; 47 | flex-basis: auto; 48 | border-radius: 50%; 49 | } 50 | .Task .Circle .Signal { 51 | position: absolute; 52 | top: 50%; 53 | left: 50%; 54 | transform: translate(-50%, -50%); 55 | width: 8px; 56 | height: 8px; 57 | opacity: .8; 58 | border-radius: 50%; 59 | border: 1px solid; 60 | content: ""; 61 | animation: signal 1.2s infinite ease-in-out; 62 | } 63 | .Task .text { 64 | flex-grow: 5; 65 | position: relative; 66 | overflow: hidden; 67 | white-space: nowrap; 68 | text-overflow: ellipsis; 69 | } 70 | .Task .text p { 71 | display: inline-block; 72 | position: relative; 73 | } 74 | .Task .Estimation { 75 | flex-grow: 1; 76 | } 77 | .Task .DueDate { 78 | flex-grow: 1; 79 | flex-basis: 20px; 80 | } 81 | .Task .DueDate > span{ 82 | display: flex; 83 | flex-direction: row; 84 | align-items: center; 85 | } 86 | .Task .Repeat { 87 | flex-grow: 1; 88 | display: flex; 89 | align-items: center; 90 | } 91 | .Task .Actions { 92 | flex-grow: 2; 93 | display: flex; 94 | flex-direction: row-reverse; 95 | } 96 | .Task .Actions .IconButton { 97 | opacity: 0; 98 | } 99 | .Task:hover .Actions .IconButton { 100 | opacity: 1; 101 | } 102 | -------------------------------------------------------------------------------- /src/Task/Task.js: -------------------------------------------------------------------------------- 1 | import React, { PureComponent } from 'react'; 2 | 3 | import Circle from './Circle'; 4 | import Estimation from './Estimation'; 5 | import DueDate from './DueDate'; 6 | import Repeat from './Repeat'; 7 | import Actions from './Actions'; 8 | import './Task.css'; 9 | 10 | class Task extends PureComponent { 11 | state = { class: '' }; 12 | 13 | handleRequestDelete = () => { 14 | this.props.onRequestDelete(this.props.index); 15 | this.props.onRequestSnackbar('Task deleted'); 16 | } 17 | handleRequestDo = () => { 18 | this.props.onRequestDo(this.props.index); 19 | this.props.onRequestSnackbar('Task done'); 20 | } 21 | render() { 22 | const { 23 | color, 24 | text, 25 | estimation, 26 | due, 27 | repeat, 28 | done, 29 | signal, 30 | } = this.props; 31 | return ( 32 |
33 | 34 |

{text}

35 | 36 | 37 | 38 | this.props.onRequestEditTaskOpen(this.props.index)} 40 | onRequestDelete={this.handleRequestDelete} 41 | onRequestDo={this.handleRequestDo} 42 | done={done} 43 | /> 44 |
45 | ); 46 | } 47 | } 48 | 49 | export default Task; 50 | -------------------------------------------------------------------------------- /src/Task/TaskList.css: -------------------------------------------------------------------------------- 1 | .TaskList { 2 | padding-top: 64px; 3 | display: flex; 4 | flex-direction: column; 5 | justify-content: flex-start; 6 | flex-wrap: nowrap; 7 | flex: 1; 8 | transition: margin 200ms cubic-bezier(0.4, 0.0, 0.2, 1) !important; 9 | } 10 | .transition-container { 11 | position: relative; 12 | } 13 | .tasks-empty-state { 14 | position: absolute; 15 | top: 50vh; 16 | left: 50%; 17 | transform: translate(-50%, -50%); 18 | flex: 1; 19 | background-color: #FAFAFA; 20 | color: #78909C; 21 | display: flex; 22 | flex-direction: column; 23 | justify-content: center; 24 | align-items: center; 25 | cursor: default; 26 | user-select: none; 27 | } 28 | .tasks-empty-state h1 { 29 | font-size: 500%; 30 | margin-bottom: 0; 31 | opacity: .2; 32 | } 33 | .tasks-empty-state h4 { 34 | font-size: 200%; 35 | margin-top: 0; 36 | opacity: .25; 37 | } 38 | -------------------------------------------------------------------------------- /src/Task/TaskListContainer.js: -------------------------------------------------------------------------------- 1 | import { connect } from 'react-redux'; 2 | import { bindActionCreators } from 'redux'; 3 | 4 | import * as actionCreators from './actionCreators'; 5 | import TaskList from './TaskList'; 6 | 7 | const mapStateToProps = state => ({ 8 | tasks: state.tasks.present, 9 | showNotYetTasks: state.appProperties.showNotYetTasks, 10 | }); 11 | 12 | const mapDispatchToProps = dispatch => bindActionCreators(actionCreators, dispatch); 13 | 14 | const TaskListContainer = connect(mapStateToProps, mapDispatchToProps)(TaskList); 15 | 16 | export default TaskListContainer; 17 | -------------------------------------------------------------------------------- /src/Task/__tests__/Actions.spec.js: -------------------------------------------------------------------------------- 1 | /* eslint-env mocha, jest */ 2 | 3 | import getActualComponentFactory from '../../lib/testUtils'; 4 | import Actions from '../Actions'; 5 | 6 | const defaultProps = { 7 | done: false, 8 | }; 9 | const getActualActions = getActualComponentFactory(Actions, defaultProps); 10 | 11 | it('should render', () => { 12 | getActualActions(); 13 | }); 14 | it('should have 3 IconButton if done is false', () => { 15 | const wrapper = getActualActions(); 16 | expect(wrapper.find('IconButton').length).toEqual(3); 17 | }); 18 | it('should have 1 IconButton if done is true', () => { 19 | const wrapper = getActualActions({ done: true }); 20 | expect(wrapper.find('IconButton').length).toEqual(1); 21 | }); 22 | -------------------------------------------------------------------------------- /src/Task/__tests__/Circle.spec.js: -------------------------------------------------------------------------------- 1 | /* eslint-env mocha, jest */ 2 | 3 | import getActualComponentFactory from '../../lib/testUtils'; 4 | import Circle from '../Circle'; 5 | 6 | const getActualCircle = getActualComponentFactory(Circle, {}); 7 | 8 | it('should render', () => { 9 | getActualCircle(); 10 | }); 11 | it('should be a div', () => { 12 | const wrapper = getActualCircle(); 13 | expect(wrapper.is('div.Circle')).toEqual(true); 14 | }); 15 | it('should have a div.Signal if signal prop is true', () => { 16 | const wrapper = getActualCircle({ signal: true }); 17 | expect(wrapper.find('div.Signal').length).toEqual(1); 18 | }); 19 | -------------------------------------------------------------------------------- /src/Task/__tests__/DueDate.spec.js: -------------------------------------------------------------------------------- 1 | /* eslint-env mocha, jest */ 2 | 3 | import getActualComponentFactory from '../../lib/testUtils'; 4 | import DueDate from '../DueDate'; 5 | 6 | const defaultProps = { 7 | due: '', 8 | }; 9 | const getActualDueDate = getActualComponentFactory(DueDate, defaultProps); 10 | 11 | it('should render', () => { 12 | getActualDueDate(); 13 | }); 14 | it('should be a span', () => { 15 | const wrapper = getActualDueDate(); 16 | expect(wrapper.is('span.DueDate')).toEqual(true); 17 | }); 18 | it('should have a small if due is tomorrow', () => { 19 | const wrapper = getActualDueDate({ due: 'tomorrow' }); 20 | expect(wrapper.find('small').length).toBeGreaterThan(0); 21 | }); 22 | it('should have a small if due is today', () => { 23 | const wrapper = getActualDueDate({ due: 'today' }); 24 | expect(wrapper.find('small').length).toBeGreaterThan(0); 25 | }); 26 | -------------------------------------------------------------------------------- /src/Task/__tests__/EditTaskDialog.spec.js: -------------------------------------------------------------------------------- 1 | /* eslint-env mocha, jest */ 2 | 3 | import getActualComponentFactory from '../../lib/testUtils'; 4 | import EditTaskDialog from '../EditTaskDialog'; 5 | 6 | const defaultProps = { 7 | open: false, 8 | task: '', 9 | onRequestClose() {}, 10 | onRequestEdit() {}, 11 | }; 12 | const getActualDialog = getActualComponentFactory(EditTaskDialog, defaultProps); 13 | 14 | it('should render', () => { 15 | getActualDialog(); 16 | }); 17 | it('should be a Dialog', () => { 18 | const wrapper = getActualDialog(); 19 | expect(wrapper.is('Dialog')).toBe(true); 20 | }); 21 | it('should have 1 Dialog', () => { 22 | const wrapper = getActualDialog(); 23 | expect(wrapper.find('Dialog').length).toBe(1); 24 | }); 25 | it('should set Dialog open based on props', () => { 26 | const wrapper = getActualDialog({ open: true }); 27 | expect(wrapper.find('Dialog').prop('open')).toBe(true); 28 | }); 29 | it('should set TextField defaultValue based on props', () => { 30 | const wrapper = getActualDialog({ 31 | task: 'a cool task', 32 | }); 33 | expect(wrapper.find('TextField').prop('defaultValue')).toBe('a cool task'); 34 | }); 35 | it('should call onRequestClose inside close FlatButton onClick', (done) => { 36 | const wrapper = getActualDialog({ 37 | onRequestClose() { 38 | done(); 39 | }, 40 | }); 41 | wrapper.find('Dialog').prop('actions')[1].props.onClick(); 42 | }); 43 | it('should call onRequestEdit inside edit FlatButton onClick', (done) => { 44 | const wrapper = getActualDialog({ 45 | onRequestEdit() { 46 | done(); 47 | }, 48 | }); 49 | wrapper.find('Dialog').prop('actions')[0].props.onClick(); 50 | }); 51 | 52 | it('should set FlatButton disabled based on state', () => { 53 | const wrapper = getActualDialog(); 54 | wrapper.find('TextField').props().onChange({ target: { value: 'a cool task' } }); 55 | wrapper.update(); 56 | expect(wrapper.find('Dialog').prop('actions')[0].props.disabled).toBe(false); 57 | }); 58 | -------------------------------------------------------------------------------- /src/Task/__tests__/Estimation.spec.js: -------------------------------------------------------------------------------- 1 | /* eslint-env mocha, jest */ 2 | 3 | import getActualComponentFactory from '../../lib/testUtils'; 4 | import Estimation from '../Estimation'; 5 | 6 | const getActualEstimation = getActualComponentFactory(Estimation, {}); 7 | 8 | it('should render', () => { 9 | getActualEstimation(); 10 | }); 11 | it('should be a small', () => { 12 | const wrapper = getActualEstimation(); 13 | expect(wrapper.is('small.Estimation')).toEqual(true); 14 | }); 15 | -------------------------------------------------------------------------------- /src/Task/__tests__/Repeat.spec.js: -------------------------------------------------------------------------------- 1 | /* eslint-env mocha, jest */ 2 | 3 | import RepeatIcon from 'material-ui/svg-icons/av/repeat'; 4 | 5 | import getActualComponentFactory from '../../lib/testUtils'; 6 | import Repeat from '../Repeat'; 7 | 8 | const defaultProps = { 9 | repeat: 0, 10 | }; 11 | const getActualRepeat = getActualComponentFactory(Repeat, defaultProps); 12 | 13 | 14 | it('should render', () => { 15 | getActualRepeat(); 16 | }); 17 | it('should be a small', () => { 18 | const wrapper = getActualRepeat(); 19 | expect(wrapper.is('small.Repeat')).toEqual(true); 20 | }); 21 | it('should have 0 RepeatIcon if repeat is 0', () => { 22 | const wrapper = getActualRepeat({ repeat: '0' }); 23 | expect(wrapper.find(RepeatIcon).length).toEqual(0); 24 | }); 25 | it('should have some RepeatIcon if repeat is not 0', () => { 26 | const wrapper = getActualRepeat({ repeat: '3' }); 27 | expect(wrapper.find(RepeatIcon).length).toBeGreaterThan(0); 28 | }); 29 | -------------------------------------------------------------------------------- /src/Task/__tests__/Task.spec.js: -------------------------------------------------------------------------------- 1 | /* eslint-env mocha, jest */ 2 | 3 | import getActualComponentFactory from '../../lib/testUtils'; 4 | import Task from '../Task'; 5 | 6 | const defaultProps = { 7 | index: 0, 8 | color: 'red', 9 | text: 'a cool task', 10 | estimation: '60', 11 | due: '', 12 | repeat: '', 13 | done: false, 14 | onRequestDo() {}, 15 | onRequestDelete() {}, 16 | onRequestSnackbar() {}, 17 | onRequestEditTaskOpen() {}, 18 | }; 19 | const getActualTask = getActualComponentFactory(Task, defaultProps); 20 | 21 | jest.useFakeTimers(); 22 | 23 | it('should render', () => { 24 | getActualTask(); 25 | }); 26 | it('should be a div', () => { 27 | const wrapper = getActualTask(); 28 | expect(wrapper.is('div.Task')).toBe(true); 29 | }); 30 | it('should have 1 text div', () => { 31 | const wrapper = getActualTask(); 32 | expect(wrapper.find('div.text').length).toBe(1); 33 | }); 34 | it('should have 1 Estimation', () => { 35 | const wrapper = getActualTask(); 36 | expect(wrapper.find('Estimation').length).toBe(1); 37 | }); 38 | it('should have 1 DueDate', () => { 39 | const wrapper = getActualTask(); 40 | expect(wrapper.find('DueDate').length).toBe(1); 41 | }); 42 | it('should have 1 Repeat', () => { 43 | const wrapper = getActualTask(); 44 | expect(wrapper.find('Repeat').length).toBe(1); 45 | }); 46 | it('should have 1 Actions', () => { 47 | const wrapper = getActualTask(); 48 | expect(wrapper.find('Actions').length).toBe(1); 49 | }); 50 | 51 | it('should set Circle color based on props', () => { 52 | const wrapper = getActualTask({ color: 'blue' }); 53 | expect(wrapper.find('Circle').prop('color')).toBe('blue'); 54 | }); 55 | it('should set Circle signal based on props', () => { 56 | const wrapper = getActualTask({ signal: true }); 57 | expect(wrapper.find('Circle').prop('signal')).toBe(true); 58 | }); 59 | it('should set text div text based on props', () => { 60 | const wrapper = getActualTask({ text: 'a cooler idea' }); 61 | expect(wrapper.find('div.text').text()).toBe('a cooler idea'); 62 | }); 63 | it('should set Estimation estimation based on props', () => { 64 | const wrapper = getActualTask({ estimation: '30' }); 65 | expect(wrapper.find('Estimation').prop('estimation')).toBe('30'); 66 | }); 67 | it('should set DueDate due based on props', () => { 68 | const wrapper = getActualTask({ due: 'tomorrow' }); 69 | expect(wrapper.find('DueDate').prop('due')).toBe('tomorrow'); 70 | }); 71 | it('should set Repeat repeat based on props', () => { 72 | const wrapper = getActualTask({ repeat: '5' }); 73 | expect(wrapper.find('Repeat').prop('repeat')).toBe('5'); 74 | }); 75 | it('should set Actions done based on props', () => { 76 | const wrapper = getActualTask({ done: true }); 77 | expect(wrapper.find('Actions').prop('done')).toBe(true); 78 | }); 79 | 80 | it('should call onRequestDelete inside Actions onRequestDelete', () => { 81 | const wrapper = getActualTask({ 82 | index: 3, 83 | onRequestDelete(index) { 84 | expect(index).toBe(3); 85 | }, 86 | }); 87 | wrapper.find('Actions').props().onRequestDelete(); 88 | jest.runAllTimers(); 89 | }); 90 | it('should call onRequestSnackbar inside Actions onRequestDelete', () => { 91 | const wrapper = getActualTask({ 92 | onRequestSnackbar(message) { 93 | expect(message).toBe('Task deleted'); 94 | }, 95 | }); 96 | wrapper.find('Actions').props().onRequestDelete(); 97 | jest.runAllTimers(); 98 | }); 99 | it('should call onRequestDo inside Actions onRequestDo', () => { 100 | const wrapper = getActualTask({ 101 | index: 3, 102 | onRequestDo(index) { 103 | expect(index).toBe(3); 104 | }, 105 | }); 106 | wrapper.find('Actions').props().onRequestDo(); 107 | jest.runAllTimers(); 108 | }); 109 | it('should call onRequestSnackbar inside Actions onRequestDo', () => { 110 | const wrapper = getActualTask({ 111 | onRequestSnackbar(message) { 112 | expect(message).toBe('Task done'); 113 | }, 114 | }); 115 | wrapper.find('Actions').props().onRequestDo(); 116 | jest.runAllTimers(); 117 | }); 118 | it('should call onRequestEditTaskOpen inside Actions onRequestEditDialogOpen', () => { 119 | const wrapper = getActualTask({ 120 | index: 3, 121 | onRequestEditTaskOpen(index) { 122 | expect(index).toBe(3); 123 | }, 124 | }); 125 | wrapper.find('Actions').props().onRequestEditDialogOpen(); 126 | jest.runAllTimers(); 127 | }); 128 | 129 | it('should remove its done class based on state', () => { 130 | const wrapper = getActualTask({ 131 | repeat: '5', 132 | }); 133 | wrapper.find('Actions').props().onRequestDo(); 134 | wrapper.update(); 135 | jest.runAllTimers(); 136 | expect(wrapper.props().className.includes('done')).toBe(false); 137 | }); 138 | -------------------------------------------------------------------------------- /src/Task/__tests__/TaskList.spec.js: -------------------------------------------------------------------------------- 1 | /* eslint-env mocha, jest */ 2 | 3 | import getActualComponentFactory from '../../lib/testUtils'; 4 | import TaskList from '../TaskList'; 5 | 6 | const now = Date.now(); 7 | const defaultProps = { 8 | tasks: [ 9 | { 10 | task: 'an overdue task', 11 | start: now - (2 * 86400000), 12 | end: now - 86400000, 13 | done: false, 14 | }, 15 | { 16 | task: 'an open task', 17 | start: now - 86400000, 18 | end: now + 86400000, 19 | done: false, 20 | }, 21 | { 22 | task: 'a not-yet task', 23 | start: now + 86400000, 24 | end: now + (2 * 86400000), 25 | done: false, 26 | }, 27 | { 28 | task: 'a done task', 29 | start: now - 86400000, 30 | end: now, 31 | done: true, 32 | }, 33 | ], 34 | sidebarExpanded: true, 35 | showNotYetTasks: true, 36 | editTask() {}, 37 | deleteTask() {}, 38 | doTask() {}, 39 | raiseFab() {}, 40 | lowerFab() {}, 41 | undo() {}, 42 | }; 43 | const getActualTaskList = getActualComponentFactory(TaskList, defaultProps); 44 | 45 | it('should render', () => { 46 | getActualTaskList(); 47 | }); 48 | it('should be a div', () => { 49 | const wrapper = getActualTaskList(); 50 | expect(wrapper.is('div.TaskList')).toBe(true); 51 | }); 52 | it('should have 13 CSSTransitionGroup', () => { 53 | const wrapper = getActualTaskList(); 54 | expect(wrapper.find('CSSTransitionGroup').length).toBe(13); 55 | }); 56 | it('should have 4 Task', () => { 57 | const wrapper = getActualTaskList(); 58 | expect(wrapper.find('Task').length).toBe(4); 59 | }); 60 | it('should have 4 Subheader', () => { 61 | const wrapper = getActualTaskList(); 62 | expect(wrapper.find('Subheader').length).toBe(4); 63 | }); 64 | it('should have 4 Divider', () => { 65 | const wrapper = getActualTaskList(); 66 | expect(wrapper.find('Divider').length).toBe(4); 67 | }); 68 | it('should have 1 EditTaskDialog', () => { 69 | const wrapper = getActualTaskList(); 70 | expect(wrapper.find('EditTaskDialog').length).toBe(1); 71 | }); 72 | it('should have 1 Snackbar', () => { 73 | const wrapper = getActualTaskList(); 74 | expect(wrapper.find('Snackbar').length).toBe(1); 75 | }); 76 | 77 | 78 | it('should have 1 tasks-empty-state if tasks is empty', () => { 79 | const wrapper = getActualTaskList({ 80 | tasks: [], 81 | }); 82 | expect(wrapper.find('.tasks-empty-state').length).toBe(1); 83 | }); 84 | it('should have 0 Task if tasks is empty', () => { 85 | const wrapper = getActualTaskList({ 86 | tasks: [], 87 | }); 88 | expect(wrapper.find('Task').length).toBe(0); 89 | }); 90 | 91 | it('should set left margin style based on props', () => { 92 | const wrapper = getActualTaskList({ 93 | sidebarExpanded: false, 94 | }); 95 | expect(wrapper.prop('style').marginLeft).toBe(56); 96 | }); 97 | it('should set left margin style based on props if tasks is empty', () => { 98 | const wrapper = getActualTaskList({ 99 | sidebarExpanded: false, 100 | tasks: [], 101 | }); 102 | expect(wrapper.prop('style').marginLeft).toBe(56); 103 | }); 104 | it('should not show not-yet tasks if showNotYetTasks is false', () => { 105 | const wrapper = getActualTaskList({ 106 | showNotYetTasks: false, 107 | }); 108 | expect(wrapper.find('Subheader').length).toBe(3); 109 | expect(wrapper.find('Divider').length).toBe(3); 110 | expect(wrapper.find('Task').length).toBe(3); 111 | }); 112 | 113 | it('should call editTask inside EditTaskDialog onRequestEdit', () => { 114 | const wrapper = getActualTaskList({ 115 | editTask(index, { task }) { 116 | expect(task).toBe('a cool edited task'); 117 | }, 118 | }); 119 | wrapper.find('EditTaskDialog').props().onRequestEdit({ task: 'a cool edited task' }); 120 | }); 121 | it('should call deleteTask inside Task onRequestDelete', () => { 122 | const wrapper = getActualTaskList({ 123 | deleteTask(index) { 124 | expect(index).toBe(2); 125 | }, 126 | }); 127 | wrapper.find('Task').at(0).props().onRequestDelete(2); 128 | }); 129 | it('should call doTask inside Task onRequestDo', () => { 130 | const wrapper = getActualTaskList({ 131 | doTask(index) { 132 | expect(index).toBe(2); 133 | }, 134 | }); 135 | wrapper.find('Task').at(0).props().onRequestDo(2); 136 | }); 137 | it('should call raiseFab inside Task onRequestSnackbar', (done) => { 138 | const wrapper = getActualTaskList({ 139 | raiseFab() { 140 | done(); 141 | }, 142 | }); 143 | wrapper.find('Task').at(0).props().onRequestSnackbar(); 144 | }); 145 | it('should call lowerFab inside Snackbar onRequestClose', (done) => { 146 | const wrapper = getActualTaskList({ 147 | lowerFab() { 148 | done(); 149 | }, 150 | }); 151 | wrapper.find('Snackbar').props().onRequestClose(); 152 | }); 153 | it('should call undo inside Snackbar onActionClick', (done) => { 154 | const wrapper = getActualTaskList({ 155 | undo() { 156 | done(); 157 | }, 158 | }); 159 | wrapper.find('Snackbar').props().onActionClick(); 160 | }); 161 | 162 | it('should set EditTaskDialog open based on state', () => { 163 | const wrapper = getActualTaskList(); 164 | wrapper.find('Task').at(0).props().onRequestEditTaskOpen(); 165 | wrapper.update(); 166 | expect(wrapper.find('EditTaskDialog').prop('open')).toBe(true); 167 | }); 168 | it('should set EditTaskDialog task based on state', () => { 169 | const wrapper = getActualTaskList(); 170 | wrapper.find('Task').at(0).props().onRequestEditTaskOpen(1); 171 | wrapper.update(); 172 | expect(wrapper.find('EditTaskDialog').prop('task')).toBe('an open task'); 173 | }); 174 | it('should set Snackbar open based on state', () => { 175 | const wrapper = getActualTaskList(); 176 | wrapper.find('Task').at(0).props().onRequestSnackbar(); 177 | wrapper.update(); 178 | expect(wrapper.find('Snackbar').prop('open')).toBe(true); 179 | }); 180 | it('should set Snackbar message based on state', () => { 181 | const wrapper = getActualTaskList(); 182 | wrapper.find('Task').at(0).props().onRequestSnackbar('a cool message'); 183 | wrapper.update(); 184 | expect(wrapper.find('Snackbar').prop('message')).toBe('a cool message'); 185 | }); 186 | -------------------------------------------------------------------------------- /src/Task/actionCreators.js: -------------------------------------------------------------------------------- 1 | import { ActionCreators as UndoActionCreators } from 'redux-undo'; 2 | 3 | const doTask = index => ({ 4 | type: 'DO_TASK', 5 | index, 6 | }); 7 | const editTask = (index, newTask) => ({ 8 | type: 'EDIT_TASK', 9 | index, 10 | newTask, 11 | }); 12 | const deleteTask = index => ({ 13 | type: 'DELETE_TASK', 14 | index, 15 | }); 16 | const raiseFab = () => ({ 17 | type: 'RAISE_FAB', 18 | }); 19 | const lowerFab = () => ({ 20 | type: 'LOWER_FAB', 21 | }); 22 | const { undo } = UndoActionCreators; 23 | 24 | export { doTask, editTask, deleteTask, raiseFab, lowerFab, undo }; 25 | -------------------------------------------------------------------------------- /src/Task/index.js: -------------------------------------------------------------------------------- 1 | export { default } from './TaskListContainer'; 2 | -------------------------------------------------------------------------------- /src/Task/lib/__tests__/classify.spec.js: -------------------------------------------------------------------------------- 1 | /* eslint-env jest */ 2 | 3 | import classify from '../classify'; 4 | 5 | const todayStart = (new Date()).setHours(0, 0, 0, 0); 6 | 7 | const tasks = [ 8 | { 9 | task: 'an overdue task', 10 | start: todayStart - (2 * 86400000), 11 | end: todayStart - (86400000 - 1), 12 | done: false, 13 | }, 14 | { 15 | task: 'an open task with due tomorrow', 16 | start: todayStart - 86400000, 17 | end: todayStart + ((2 * 86400000) - 1), 18 | done: false, 19 | }, 20 | { 21 | task: 'an open task with due tomorrow', 22 | start: todayStart - 86400000, 23 | end: todayStart + ((3 * 86400000) - 1), 24 | done: false, 25 | }, 26 | { 27 | task: 'an open task with due today', 28 | start: todayStart - 86400000, 29 | end: todayStart + (86400000 - 1), 30 | done: false, 31 | }, 32 | { 33 | task: 'a not-yet task', 34 | start: todayStart + 86400000, 35 | end: todayStart + ((2 * 86400000) - 1), 36 | done: false, 37 | }, 38 | { 39 | task: 'a done task', 40 | start: todayStart - 86400000, 41 | end: todayStart - 1, 42 | done: true, 43 | }, 44 | ]; 45 | 46 | it('should classify into groups', () => { 47 | const { 48 | overdue, 49 | open, 50 | notYet, 51 | done, 52 | } = classify(tasks); 53 | expect(overdue.length).toBe(1); 54 | expect(open.length).toBe(3); 55 | expect(notYet.length).toBe(1); 56 | expect(done.length).toBe(1); 57 | }); 58 | it('should sort tasks properly', () => { 59 | const { open } = classify(tasks); 60 | expect(open[0].due).toBe('today'); 61 | }); 62 | it('should set overdue tasks color to red', () => { 63 | const { overdue } = classify(tasks); 64 | expect(overdue[0].color).toBe('hsl(0, 100%, 75%)'); 65 | }); 66 | it('should set overdue tasks signal to true', () => { 67 | const { overdue } = classify(tasks); 68 | expect(overdue[0].signal).toBe(true); 69 | }); 70 | it('should set not-yet tasks color to red', () => { 71 | const { notYet } = classify(tasks); 72 | expect(notYet[0].color).toBe('hsl(180, 100%, 50%)'); 73 | }); 74 | it('should set not-yet tasks signal to false', () => { 75 | const { notYet } = classify(tasks); 76 | expect(notYet[0].signal).toBe(false); 77 | }); 78 | it('should set done tasks color to red', () => { 79 | const { done } = classify(tasks); 80 | expect(done[0].color).toBe('#AB47BC'); 81 | }); 82 | it('should set done tasks signal to false', () => { 83 | const { done } = classify(tasks); 84 | expect(done[0].signal).toBe(false); 85 | }); 86 | it('should set today due (if any)', () => { 87 | const { open } = classify(tasks); 88 | expect(open[0].due).toBe('today'); 89 | }); 90 | it('should set tomorrow due (if any)', () => { 91 | const { open } = classify(tasks); 92 | expect(open[1].due).toBe('tomorrow'); 93 | }); 94 | -------------------------------------------------------------------------------- /src/Task/lib/__tests__/cumulate.spec.js: -------------------------------------------------------------------------------- 1 | /* eslint-env jest */ 2 | 3 | import cumulate from '../cumulate'; 4 | 5 | const todayStart = (new Date()).setHours(0, 0, 0, 0); 6 | 7 | const tasks = { 8 | overdue: [ 9 | { 10 | task: 'an overdue task', 11 | start: new Date(todayStart - (2 * 86400000)), 12 | end: new Date(todayStart - (86400000 - 1)), 13 | done: false, 14 | }, 15 | ], 16 | open: [ 17 | { 18 | task: 'an open task with due tomorrow', 19 | start: new Date(todayStart - 86400000), 20 | end: new Date(todayStart + ((2 * 86400000) - 1)), 21 | done: false, 22 | }, 23 | { 24 | task: 'an open task with due tomorrow', 25 | start: new Date(todayStart - 86400000), 26 | end: new Date(todayStart + ((3 * 86400000) - 1)), 27 | done: false, 28 | }, 29 | { 30 | task: 'an open task with due today', 31 | start: new Date(todayStart - 86400000), 32 | end: new Date(todayStart + (86400000 - 1)), 33 | done: false, 34 | }, 35 | ], 36 | notYet: [ 37 | { 38 | task: 'a not-yet task', 39 | start: new Date(todayStart + 86400000), 40 | end: new Date(todayStart + ((2 * 86400000) - 1)), 41 | done: false, 42 | }, 43 | ], 44 | done: [ 45 | { 46 | task: 'a done task', 47 | start: new Date(todayStart - 86400000), 48 | end: new Date(todayStart - 1), 49 | done: true, 50 | }, 51 | ], 52 | }; 53 | 54 | it('should count cumulative frequencies of task groups', () => { 55 | const { open, notYet, done } = cumulate(tasks); 56 | expect(open).toBe(1); 57 | expect(notYet).toBe(4); 58 | expect(done).toBe(5); 59 | }); 60 | it('should set all cumulative frequencies to zero if input is null', () => { 61 | const { open, notYet, done } = cumulate(null); 62 | expect(open).toBe(0); 63 | expect(notYet).toBe(0); 64 | expect(done).toBe(0); 65 | }); 66 | it('should set all cumulative frequencies to zero if input is undefined', () => { 67 | const { open, notYet, done } = cumulate(); 68 | expect(open).toBe(0); 69 | expect(notYet).toBe(0); 70 | expect(done).toBe(0); 71 | }); 72 | -------------------------------------------------------------------------------- /src/Task/lib/classify.js: -------------------------------------------------------------------------------- 1 | const setDue = (remaining) => { 2 | if (remaining < 86400000) { 3 | return 'today'; 4 | } else if (remaining < 172800000) { 5 | return 'tomorrow'; 6 | } 7 | return ''; 8 | }; 9 | const setColor = (remaining, total) => { 10 | const ratio = remaining / total; 11 | return `hsl(${ratio * 180}, 100%, 50%)`; 12 | }; 13 | const setSignal = (remaining, total) => remaining / total < 0.25; 14 | const classify = (tasks) => { 15 | const classifiedTasks = { 16 | overdue: [], 17 | open: [], 18 | notYet: [], 19 | done: [], 20 | }; 21 | const now = Date.now(); 22 | tasks.forEach((task, index) => { 23 | const { start } = task; 24 | let { end } = task; 25 | end += 999; 26 | if (task.done) { 27 | classifiedTasks.done.push({ 28 | ...task, 29 | color: '#AB47BC', 30 | signal: false, 31 | index, 32 | }); 33 | } else if (now < start) { 34 | const remaining = end - now; 35 | classifiedTasks.notYet.push({ 36 | ...task, 37 | due: setDue(remaining), 38 | color: 'hsl(180, 100%, 50%)', 39 | signal: false, 40 | index, 41 | }); 42 | } else if (now <= end) { 43 | const remaining = end - now; 44 | const total = end - start; 45 | classifiedTasks.open.push({ 46 | ...task, 47 | due: setDue(remaining), 48 | color: setColor(remaining, total), 49 | signal: setSignal(remaining, total), 50 | index, 51 | }); 52 | } else { 53 | classifiedTasks.overdue.push({ 54 | ...task, 55 | color: 'hsl(0, 100%, 75%)', 56 | signal: true, 57 | index, 58 | }); 59 | } 60 | }); 61 | Object.keys(classifiedTasks).forEach(group => 62 | classifiedTasks[group].sort((a, b) => { 63 | const aRatio = (now - a.start) / (a.end - a.start); 64 | const bRatio = (now - b.start) / (b.end - b.start); 65 | return bRatio - aRatio; 66 | })); 67 | return classifiedTasks; 68 | }; 69 | 70 | export default classify; 71 | -------------------------------------------------------------------------------- /src/Task/lib/cumulate.js: -------------------------------------------------------------------------------- 1 | const cumulate = (tasks) => { 2 | const cumulativeFrequencies = { 3 | overdue: 0, 4 | open: 0, 5 | notYet: 0, 6 | done: 0, 7 | }; 8 | if (!tasks) { 9 | return cumulativeFrequencies; 10 | } 11 | cumulativeFrequencies.open = tasks.overdue.length; 12 | cumulativeFrequencies.notYet = tasks.open.length + cumulativeFrequencies.open; 13 | cumulativeFrequencies.done = tasks.notYet.length + cumulativeFrequencies.notYet; 14 | return cumulativeFrequencies; 15 | }; 16 | 17 | export default cumulate; 18 | -------------------------------------------------------------------------------- /src/TitilliumWeb-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mkermani144/wanna/7973a3d80825c99cb31d6a89e436fae075c2c7f7/src/TitilliumWeb-Regular.ttf -------------------------------------------------------------------------------- /src/__tests__/e2e/ideas.e2e.js: -------------------------------------------------------------------------------- 1 | /* eslint-env mocha, jasmine */ 2 | 3 | import { 4 | createDriver, 5 | utilsFactory, 6 | } from '../../lib/e2eUtils'; 7 | 8 | const driver = createDriver(); 9 | const { 10 | init, 11 | click, 12 | type, 13 | count, 14 | wait, 15 | pressEnter, 16 | pressRightArrow, 17 | close, 18 | } = utilsFactory(driver); 19 | 20 | jasmine.DEFAULT_TIMEOUT_INTERVAL = 600000; 21 | 22 | it('should test ideas functionality', async (done) => { 23 | // Open 24 | await init(); 25 | 26 | // Add ideas 27 | await click('#ideas'); 28 | await click('#plus-fab'); 29 | await click('#lightbulb-fab'); 30 | await type('.NewIdeaDialog input', 'a cool idea'); 31 | await click('#add-and-continue'); 32 | await type('.NewIdeaDialog input', 'another cool idea'); 33 | await click('#add-and-continue'); 34 | await type('.NewIdeaDialog input', 'even another cool idea'); 35 | await click('#add-and-finish'); 36 | await wait(500); 37 | let numberOfIdeas = await count('.Idea'); 38 | expect(numberOfIdeas).toBe(3); 39 | await wait(500); 40 | 41 | // Delete idea 42 | await click('.Idea:first-of-type .delete'); 43 | await wait(500); 44 | numberOfIdeas = await count('.Idea'); 45 | expect(numberOfIdeas).toBe(2); 46 | await wait(500); 47 | 48 | // Edit idea 49 | await click('.Idea:first-of-type .edit'); 50 | await type('.EditIdeaDialog input', 'an edited cool idea'); 51 | await click('#edit'); 52 | await wait(500); 53 | numberOfIdeas = await count('.Idea'); 54 | expect(numberOfIdeas).toBe(2); 55 | await wait(500); 56 | 57 | // Convert idea 58 | await click('.Idea:first-of-type .convert'); 59 | await type('.ConvertIdeaDialog input', 'a cool task from a cool idea'); 60 | await click('#end input'); 61 | await pressRightArrow(5); 62 | await pressEnter(); 63 | await type('#estimated-time', 10); 64 | await wait(500); 65 | await click('#add-and-continue'); 66 | await type('.ConvertIdeaDialog input', 'another cool task from a cool idea'); 67 | await click('#start input'); 68 | await pressRightArrow(2); 69 | await pressEnter(); 70 | await wait(500); 71 | await click('#end input'); 72 | await pressRightArrow(4); 73 | await pressEnter(); 74 | await type('#estimated-time', 20); 75 | await wait(500); 76 | await click('#add-and-finish'); 77 | await wait(500); 78 | numberOfIdeas = await count('.Idea'); 79 | expect(numberOfIdeas).toBe(1); 80 | await wait(500); 81 | await click('#tasks'); 82 | const numberOfTasks = await count('.Task'); 83 | expect(numberOfTasks).toBe(2); 84 | 85 | // Close 86 | await close(); 87 | done(); 88 | }); 89 | -------------------------------------------------------------------------------- /src/__tests__/e2e/settings.e2e.js: -------------------------------------------------------------------------------- 1 | /* eslint-env mocha, jasmine */ 2 | 3 | import { 4 | createDriver, 5 | utilsFactory, 6 | } from '../../lib/e2eUtils'; 7 | 8 | const driver = createDriver(); 9 | const { 10 | init, 11 | click, 12 | type, 13 | count, 14 | wait, 15 | pressEnter, 16 | pressRightArrow, 17 | close, 18 | } = utilsFactory(driver); 19 | 20 | jasmine.DEFAULT_TIMEOUT_INTERVAL = 600000; 21 | 22 | it('should test settings functionality', async (done) => { 23 | // Open 24 | await init(); 25 | 26 | // Add tasks 27 | await click('#plus-fab'); 28 | await click('#done-fab'); 29 | await type('.NewTaskDialog input', 'a cool task'); 30 | await click('#end input'); 31 | await pressRightArrow(5); 32 | await pressEnter(); 33 | await type('#estimated-time', 10); 34 | await wait(500); 35 | await click('#add-and-continue'); 36 | await type('.NewTaskDialog input', 'another cool task'); 37 | await click('#start input'); 38 | await pressRightArrow(2); 39 | await pressEnter(); 40 | await wait(500); 41 | await click('#end input'); 42 | await pressRightArrow(4); 43 | await pressEnter(); 44 | await type('#estimated-time', 20); 45 | await wait(500); 46 | await click('#add-and-finish'); 47 | let numberOfTasks = await count('.Task'); 48 | expect(numberOfTasks).toBe(2); 49 | await wait(500); 50 | 51 | // Toggle not-yet tasks 52 | await click('#settings'); 53 | await click('#not-yet-tasks input'); 54 | 55 | // Count tasks 56 | await click('#tasks'); 57 | numberOfTasks = await count('.Task'); 58 | expect(numberOfTasks).toBe(1); 59 | 60 | // Close 61 | await close(); 62 | done(); 63 | }); 64 | -------------------------------------------------------------------------------- /src/__tests__/e2e/tasks.e2e.js: -------------------------------------------------------------------------------- 1 | /* eslint-env mocha, jasmine */ 2 | 3 | import { 4 | createDriver, 5 | utilsFactory, 6 | } from '../../lib/e2eUtils'; 7 | 8 | const driver = createDriver(); 9 | const { 10 | init, 11 | click, 12 | type, 13 | count, 14 | wait, 15 | pressEnter, 16 | pressRightArrow, 17 | close, 18 | } = utilsFactory(driver); 19 | 20 | jasmine.DEFAULT_TIMEOUT_INTERVAL = 600000; 21 | 22 | it('should test tasks functionality', async (done) => { 23 | // Open 24 | await init(); 25 | 26 | // Add tasks 27 | await click('#plus-fab'); 28 | await click('#done-fab'); 29 | await type('.NewTaskDialog input', 'a cool task'); 30 | await click('#end input'); 31 | // await pressRightArrow(5); 32 | 33 | await pressEnter(); 34 | await type('#estimated-time', 10); 35 | await wait(500); 36 | await click('#add-and-continue'); 37 | await type('.NewTaskDialog input', 'another cool task'); 38 | await click('#start input'); 39 | await pressRightArrow(2); 40 | await pressEnter(); 41 | await wait(500); 42 | await click('#end input'); 43 | await pressRightArrow(4); 44 | await pressEnter(); 45 | await type('#estimated-time', 20); 46 | await wait(500); 47 | await click('#add-and-finish'); 48 | await wait(500); 49 | let numberOfTasks = await count('.Task'); 50 | expect(numberOfTasks).toBe(2); 51 | await wait(500); 52 | 53 | // Delete task 54 | await click('.Task .delete'); 55 | await wait(500); 56 | numberOfTasks = await count('.Task'); 57 | expect(numberOfTasks).toBe(1); 58 | await wait(500); 59 | 60 | // Edit task 61 | await click('.Task .edit'); 62 | await type('.EditTaskDialog input', 'an edited cool task'); 63 | await click('#edit'); 64 | await wait(500); 65 | numberOfTasks = await count('.Task'); 66 | expect(numberOfTasks).toBe(1); 67 | await wait(500); 68 | 69 | // Mark task as done 70 | await click('.Task .mark-as-done'); 71 | await wait(500); 72 | numberOfTasks = await count('.Task'); 73 | expect(numberOfTasks).toBe(1); 74 | await wait(500); 75 | 76 | // Close 77 | await close(); 78 | done(); 79 | }); 80 | -------------------------------------------------------------------------------- /src/__tests__/reducer.spec.js: -------------------------------------------------------------------------------- 1 | /* eslint-env mocha, jest */ 2 | 3 | import rootReducer from '../reducer'; 4 | 5 | it('should return some state if no state and action is provided', () => { 6 | const expected = { 7 | tasks: { 8 | history: { 9 | present: [], 10 | }, 11 | present: [], 12 | }, 13 | ideas: { 14 | history: { 15 | present: [], 16 | }, 17 | present: [], 18 | }, 19 | appProperties: {}, 20 | appUI: {}, 21 | }; 22 | const actual = rootReducer(undefined, {}); 23 | expect(actual).toEqual(expected); 24 | }); 25 | -------------------------------------------------------------------------------- /src/icons/__tests__/fork.spec.js: -------------------------------------------------------------------------------- 1 | /* eslint-env mocha, jest */ 2 | 3 | import getActualComponentFactory from '../../lib/testUtils'; 4 | import Fork from '../fork'; 5 | 6 | const getActualFork = getActualComponentFactory(Fork, {}); 7 | 8 | it('should render', () => { 9 | getActualFork(); 10 | }); 11 | it('should have 1 path', () => { 12 | const wrapper = getActualFork(); 13 | expect(wrapper.find('path').length).toBeGreaterThan(0); 14 | }); 15 | -------------------------------------------------------------------------------- /src/icons/fork.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import SvgIcon from 'material-ui/SvgIcon'; 3 | 4 | const Fork = props => ( 5 | 6 | 7 | 8 | ); 9 | 10 | export default Fork; 11 | -------------------------------------------------------------------------------- /src/index.css: -------------------------------------------------------------------------------- 1 | @font-face { 2 | font-family: 'Titillium'; 3 | src: url('./TitilliumWeb-Regular.ttf'); 4 | } 5 | body { 6 | margin: 0; 7 | padding: 0; 8 | font-family: sans-serif; 9 | } 10 | * { 11 | font-family: 'Titillium', sans-serif; 12 | } 13 | p, small { 14 | cursor: default; 15 | } 16 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | /* eslint-env browser */ 2 | 3 | import React from 'react'; 4 | import ReactDOM from 'react-dom'; 5 | 6 | import AppContainer from './AppContainer'; 7 | import './index.css'; 8 | 9 | ReactDOM.render(, document.getElementById('root')); 10 | -------------------------------------------------------------------------------- /src/lib/__tests__/date.spec.js: -------------------------------------------------------------------------------- 1 | /* eslint-env mocha, jest */ 2 | 3 | import { 4 | parse, 5 | dayStart, 6 | dayEnd, 7 | addDays, 8 | } from '../date'; 9 | 10 | const sampleUnixDate = 1511100000000; 11 | 12 | it('should parse date correctly', () => { 13 | const expected = sampleUnixDate; 14 | const actual = parse(new Date(sampleUnixDate)); 15 | expect(actual).toBe(expected); 16 | }); 17 | it('should return day start correctly', () => { 18 | const expected = (new Date(sampleUnixDate)).setHours(0, 0, 0, 0); 19 | const actual = dayStart(sampleUnixDate); 20 | expect(actual).toBe(expected); 21 | }); 22 | it('should return day end correctly', () => { 23 | const expected = (new Date(sampleUnixDate)).setHours(23, 59, 59, 999); 24 | const actual = dayEnd(sampleUnixDate); 25 | expect(actual).toBe(expected); 26 | }); 27 | it('should add days to a date correctly', () => { 28 | const expected = 1511359200000; 29 | const actual = addDays(3, sampleUnixDate); 30 | expect(actual).toBe(expected); 31 | }); 32 | -------------------------------------------------------------------------------- /src/lib/constants.js: -------------------------------------------------------------------------------- 1 | export const FABRaiseWindowWidthTreshold = 768; 2 | export const FABBottom = 24; 3 | export const FABMiniBottomClosed = 32; 4 | export const FABRaisedBottom = 72; 5 | export const FABMiniRaisedBottomClosed = 80; 6 | export const FABMiniBottomOpen1 = 92; 7 | export const FABMiniBottomOpenDiff = 50; 8 | export const FABRight = 24; 9 | export const FABMiniRight = 32; 10 | 11 | export const navMiniWidth = 56; 12 | export const navExpandedWidth = 200; 13 | 14 | export const transitionEnterTimeout = 170; 15 | export const transitionLeaveTimeout = 150; 16 | 17 | -------------------------------------------------------------------------------- /src/lib/database.js: -------------------------------------------------------------------------------- 1 | /* eslint-env browser */ 2 | 3 | let fs; 4 | let path; 5 | let os; 6 | if (process.env.REACT_APP_E2E) { 7 | fs = { 8 | readFileSync() {}, 9 | writeFileSync() {}, 10 | }; 11 | path = { join() {} }; 12 | os = { homedir() {} }; 13 | } else { 14 | fs = window.require('fs'); 15 | path = window.require('path'); 16 | os = window.require('os'); 17 | } 18 | 19 | let parentPath = os.homedir(); 20 | if (process.env.NODE_ENV === 'development') { 21 | parentPath = './'; 22 | } 23 | 24 | const dbPath = path.join(parentPath, '.wanna/db'); 25 | 26 | const fetchDatabaseState = () => { 27 | let data = fs.readFileSync(dbPath, 'utf-8'); 28 | if (data) { 29 | data = JSON.parse(data); 30 | return { 31 | tasks: { 32 | past: [], 33 | present: data.tasks, 34 | future: [], 35 | history: [], 36 | }, 37 | ideas: { 38 | past: [], 39 | present: data.ideas, 40 | future: [], 41 | history: [], 42 | }, 43 | appProperties: data.appProperties, 44 | appUI: { 45 | FABRaised: false, 46 | currentTab: data.appProperties.startupTab, 47 | }, 48 | }; 49 | } 50 | return null; 51 | }; 52 | const update = (state) => { 53 | fs.writeFileSync(dbPath, JSON.stringify({ 54 | tasks: state.tasks.present, 55 | ideas: state.ideas.present, 56 | appProperties: state.appProperties, 57 | }), 'utf-8'); 58 | }; 59 | 60 | 61 | export { fetchDatabaseState, update }; 62 | -------------------------------------------------------------------------------- /src/lib/date.js: -------------------------------------------------------------------------------- 1 | import * as R from 'ramda'; 2 | 3 | const unixNow = Date.now; 4 | const unixToDateObject = unixTime => new Date(unixTime); 5 | const dateObjectUnixDayStart = dateObject => dateObject.setHours(0, 0, 0, 0); 6 | const dateObjectUnixDayEnd = dateObject => dateObject.setHours(23, 59, 59, 999); 7 | const multiply86400000 = R.multiply(86400000); 8 | 9 | const { parse } = Date; 10 | const dayStart = R.pipe(unixToDateObject, dateObjectUnixDayStart); 11 | const dayEnd = R.pipe(unixToDateObject, dateObjectUnixDayEnd); 12 | const todayStart = R.pipe(unixNow, dayStart); 13 | 14 | const addDays = R.pipe(multiply86400000, R.add); 15 | const uncurriedAddDays = R.uncurryN(2, addDays); 16 | 17 | export { 18 | parse, 19 | dayStart, 20 | todayStart, 21 | dayEnd, 22 | uncurriedAddDays as addDays, 23 | }; 24 | -------------------------------------------------------------------------------- /src/lib/e2eUtils.js: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line import/no-extraneous-dependencies 2 | import webdriver from 'selenium-webdriver'; 3 | 4 | const { By, Key } = webdriver; 5 | 6 | const createDriver = () => new webdriver.Builder() 7 | .forBrowser('chrome') 8 | .build(); 9 | const utilsFactory = driver => ({ 10 | init() { 11 | return driver.navigate().to('http://localhost:3000'); 12 | }, 13 | click(selector) { 14 | return driver.findElement(By.css(selector)).click(); 15 | }, 16 | async type(selector, text) { 17 | await driver.findElement(By.css(selector)).clear(); 18 | return driver.findElement(By.css(selector)).sendKeys(text); 19 | }, 20 | async count(selector) { 21 | return (await driver.findElements(By.css(selector))).length; 22 | }, 23 | wait(ms) { 24 | return driver.sleep(ms); 25 | }, 26 | pressEnter() { 27 | return driver.actions().sendKeys(Key.RETURN).perform(); 28 | }, 29 | pressRightArrow(times) { 30 | return driver.actions().sendKeys(...Array(times).fill(Key.ARROW_RIGHT)).perform(); 31 | }, 32 | close() { 33 | return driver.close(); 34 | }, 35 | }); 36 | 37 | export { 38 | createDriver, 39 | utilsFactory, 40 | }; 41 | -------------------------------------------------------------------------------- /src/lib/testUtils.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | // eslint-disable-next-line import/no-extraneous-dependencies 3 | import { shallow } from 'enzyme'; 4 | import R from 'ramda'; 5 | 6 | const componentory = (Component, props) => 7 | shallow(, { disableLifecycleMethods: true }); 8 | const merge = (defaultProps, props) => R.mergeAll([defaultProps, props]); 9 | 10 | const getActualComponentFactory = (Component, defaultProps) => R.pipe( 11 | R.partial(merge, [defaultProps]), 12 | R.partial(componentory, [Component]), 13 | ); 14 | 15 | export default getActualComponentFactory; 16 | -------------------------------------------------------------------------------- /src/reducer.js: -------------------------------------------------------------------------------- 1 | import { combineReducers } from 'redux'; 2 | import undoable, { includeAction } from 'redux-undo'; 3 | 4 | import taskReducer from './reducers/task'; 5 | import ideaReducer from './reducers/idea'; 6 | import appPropertiesReducer from './reducers/appProperties'; 7 | import appUIReducer from './reducers/appUI'; 8 | 9 | const rootReducer = combineReducers({ 10 | tasks: undoable(taskReducer, { 11 | limit: 1, 12 | filter: includeAction(['DO_TASK', 'DELETE_TASK']), 13 | }), 14 | ideas: undoable(ideaReducer, { 15 | limit: 1, 16 | filter: includeAction('DELETE_IDEA'), 17 | }), 18 | appProperties: appPropertiesReducer, 19 | appUI: appUIReducer, 20 | }); 21 | 22 | export default rootReducer; 23 | -------------------------------------------------------------------------------- /src/reducers/__tests__/appProperties.spec.js: -------------------------------------------------------------------------------- 1 | /* eslint-env mocha, jest */ 2 | 3 | import appPropertiesReducer from '../appProperties'; 4 | import { 5 | toggleNotYet, 6 | toggleFullscreen, 7 | changeCalendarSystem, 8 | changeFirstDayOfWeek, 9 | changeStartupTab, 10 | } from '../../Settings/actionCreators'; 11 | 12 | const defaultState = { 13 | showNotYetTasks: true, 14 | fullscreen: false, 15 | calendarSystem: 'en-US', 16 | firstDayOfWeek: 1, 17 | startupTab: 'tasks', 18 | }; 19 | 20 | const getExpectedState = (props = {}) => Object.assign({}, defaultState, props); 21 | 22 | it('should return some state if no state and action is provided', () => { 23 | const expected = {}; 24 | const actual = appPropertiesReducer(undefined, {}); 25 | expect(actual).toEqual(expected); 26 | }); 27 | it('should update showNotYetTasks of state', () => { 28 | const expected = getExpectedState({ showNotYetTasks: false }); 29 | const action = toggleNotYet(false); 30 | const actual = appPropertiesReducer(defaultState, action); 31 | expect(actual).toEqual(expected); 32 | }); 33 | it('should update fullscreen of state', () => { 34 | const expected = getExpectedState({ fullscreen: true }); 35 | const action = toggleFullscreen(true); 36 | const actual = appPropertiesReducer(defaultState, action); 37 | expect(actual).toEqual(expected); 38 | }); 39 | it('should update calendarSystem of state', () => { 40 | const expected = getExpectedState({ calendarSystem: 'fa-IR' }); 41 | const action = changeCalendarSystem('fa-IR'); 42 | const actual = appPropertiesReducer(defaultState, action); 43 | expect(actual).toEqual(expected); 44 | }); 45 | it('should update firstDayOfWeek of state', () => { 46 | const expected = getExpectedState({ firstDayOfWeek: 6 }); 47 | const action = changeFirstDayOfWeek(6); 48 | const actual = appPropertiesReducer(defaultState, action); 49 | expect(actual).toEqual(expected); 50 | }); 51 | it('should update startupTab of state', () => { 52 | const expected = getExpectedState({ startupTab: 'ideas' }); 53 | const action = changeStartupTab('ideas'); 54 | const actual = appPropertiesReducer(defaultState, action); 55 | expect(actual).toEqual(expected); 56 | }); 57 | -------------------------------------------------------------------------------- /src/reducers/__tests__/appUI.spec.js: -------------------------------------------------------------------------------- 1 | /* eslint-env mocha, jest */ 2 | 3 | import appUIReducer from '../appUI'; 4 | import { 5 | raiseFab as ideaRaiseFab, 6 | lowerFab as ideaLowerFab, 7 | } from '../../Idea/actionCreators'; 8 | import { 9 | raiseFab as taskRaiseFab, 10 | lowerFab as taskLowerFab, 11 | } from '../../Task/actionCreators'; 12 | import changeTab from '../../Sidebar/actionCreators'; 13 | 14 | const defaultState = { 15 | FABRaised: false, 16 | currentTab: 'tasks', 17 | }; 18 | 19 | const getExpectedState = (props = {}) => Object.assign({}, defaultState, props); 20 | 21 | it('should return some state if no state and action is provided', () => { 22 | const expected = {}; 23 | const actual = appUIReducer(undefined, {}); 24 | expect(actual).toEqual(expected); 25 | }); 26 | it('should make FABRaised true (idea)', () => { 27 | const expected = getExpectedState({ FABRaised: true }); 28 | const action = ideaRaiseFab(); 29 | const actual = appUIReducer(defaultState, action); 30 | expect(actual).toEqual(expected); 31 | }); 32 | it('should make FABRaised false (idea)', () => { 33 | const expected = getExpectedState({ FABRaised: false }); 34 | const action = ideaLowerFab(); 35 | const actual = appUIReducer(defaultState, action); 36 | expect(actual).toEqual(expected); 37 | }); 38 | it('should make FABRaised true (task)', () => { 39 | const expected = getExpectedState({ FABRaised: true }); 40 | const action = taskRaiseFab(); 41 | const actual = appUIReducer(defaultState, action); 42 | expect(actual).toEqual(expected); 43 | }); 44 | it('should make FABRaised false (task)', () => { 45 | const expected = getExpectedState({ FABRaised: false }); 46 | const action = taskLowerFab(); 47 | const actual = appUIReducer(defaultState, action); 48 | expect(actual).toEqual(expected); 49 | }); 50 | it('should update currentTab', () => { 51 | const expected = getExpectedState({ currentTab: 'ideas' }); 52 | const action = changeTab('ideas'); 53 | const actual = appUIReducer(defaultState, action); 54 | expect(actual).toEqual(expected); 55 | }); 56 | -------------------------------------------------------------------------------- /src/reducers/__tests__/idea.spec.js: -------------------------------------------------------------------------------- 1 | /* eslint-env mocha, jest */ 2 | 3 | import ideaReducer from '../idea'; 4 | import { addIdea } from '../../FAB/actionCreators'; 5 | import { 6 | editIdea, 7 | deleteIdea, 8 | } from '../../Idea/actionCreators'; 9 | 10 | const defaultState = [ 11 | { 12 | idea: 'A cool idea', 13 | id: 'a', 14 | }, 15 | { 16 | idea: 'Another cool idea', 17 | id: 'b', 18 | }, 19 | { 20 | idea: 'Even another cool idea', 21 | id: 'c', 22 | }, 23 | ]; 24 | 25 | const getExpectedState = (props = {}) => Object.assign([], defaultState, props); 26 | 27 | it('should return some state if no state and action is provided', () => { 28 | const expected = []; 29 | const actual = ideaReducer(undefined, {}); 30 | expect(actual).toEqual(expected); 31 | }); 32 | it('should add new idea', () => { 33 | const expected = getExpectedState({ 34 | [defaultState.length]: { 35 | idea: 'A nice idea', 36 | id: 'd', 37 | }, 38 | }); 39 | const action = addIdea({ 40 | idea: 'A nice idea', 41 | id: 'd', 42 | }); 43 | const actual = ideaReducer(defaultState, action); 44 | expect(actual).toEqual(expected); 45 | }); 46 | it('should edit idea', () => { 47 | const expected = getExpectedState({ 48 | 1: { 49 | idea: 'A nice idea', 50 | id: 'b', 51 | }, 52 | }); 53 | const action = editIdea(1, { idea: 'A nice idea' }); 54 | const actual = ideaReducer(defaultState, action); 55 | expect(actual).toEqual(expected); 56 | }); 57 | it('should delete idea', () => { 58 | const expected = getExpectedState({ 59 | 1: undefined, 60 | }).filter(state => state); 61 | const action = deleteIdea(1); 62 | const actual = ideaReducer(defaultState, action); 63 | expect(actual).toEqual(expected); 64 | }); 65 | -------------------------------------------------------------------------------- /src/reducers/__tests__/task.spec.js: -------------------------------------------------------------------------------- 1 | /* eslint-env mocha, jest */ 2 | 3 | import taskReducer from '../task'; 4 | import { addTask as FABAddTask } from '../../FAB/actionCreators'; 5 | import { addTask as ideaAddTask } from '../../Idea/actionCreators'; 6 | import { 7 | doTask, 8 | editTask, 9 | deleteTask, 10 | } from '../../Task/actionCreators'; 11 | 12 | const now = Date.now(); 13 | const later = Date.now() + 86399999; 14 | 15 | const defaultState = [ 16 | { 17 | task: 'A cool task', 18 | start: now, 19 | end: later, 20 | estimation: 5, 21 | repetition: '', 22 | done: false, 23 | id: 'a', 24 | }, 25 | { 26 | task: 'Another cool task', 27 | start: now, 28 | end: later, 29 | estimation: 60, 30 | repetition: 3, 31 | done: false, 32 | id: 'b', 33 | }, 34 | { 35 | task: 'Even another cool task', 36 | start: now, 37 | end: later, 38 | estimation: 120, 39 | repetition: '', 40 | done: true, 41 | id: 'c', 42 | }, 43 | ]; 44 | 45 | const getExpectedState = (props = {}) => Object.assign([], defaultState, props); 46 | 47 | it('should return some state if no state and action is provided', () => { 48 | const expected = []; 49 | const actual = taskReducer(undefined, {}); 50 | expect(actual).toEqual(expected); 51 | }); 52 | it('should add new task (via FAB)', () => { 53 | const expected = getExpectedState({ 54 | [defaultState.length]: { 55 | task: 'A nice task', 56 | start: now, 57 | end: later, 58 | estimation: 30, 59 | repetition: '', 60 | done: false, 61 | id: 'd', 62 | }, 63 | }); 64 | const action = FABAddTask({ 65 | task: 'A nice task', 66 | start: now, 67 | end: later, 68 | estimation: 30, 69 | repetition: '', 70 | done: false, 71 | id: 'd', 72 | }); 73 | const actual = taskReducer(defaultState, action); 74 | expect(actual).toEqual(expected); 75 | }); 76 | it('should add new task (via Idea)', () => { 77 | const expected = getExpectedState({ 78 | [defaultState.length]: { 79 | task: 'A nice task', 80 | start: now, 81 | end: later, 82 | estimation: 30, 83 | repetition: '', 84 | done: false, 85 | id: 'd', 86 | }, 87 | }); 88 | const action = ideaAddTask({ 89 | task: 'A nice task', 90 | start: now, 91 | end: later, 92 | estimation: 30, 93 | repetition: '', 94 | done: false, 95 | id: 'd', 96 | }); 97 | const actual = taskReducer(defaultState, action); 98 | expect(actual).toEqual(expected); 99 | }); 100 | it('should do non-repeating task', () => { 101 | const expected = getExpectedState({ 102 | 0: { 103 | task: 'A cool task', 104 | start: now, 105 | end: later, 106 | estimation: 5, 107 | repetition: '', 108 | done: true, 109 | id: 'a', 110 | }, 111 | }); 112 | const action = doTask(0); 113 | const actual = taskReducer(defaultState, action); 114 | expect(actual).toEqual(expected); 115 | }); 116 | it('should do repeating task', () => { 117 | const expected = getExpectedState({ 118 | 1: { 119 | task: 'Another cool task', 120 | start: now + (3 * 86400000), 121 | end: later + (3 * 86400000), 122 | estimation: 60, 123 | repetition: 3, 124 | done: false, 125 | id: 'b', 126 | }, 127 | }); 128 | const action = doTask(1); 129 | const actual = taskReducer(defaultState, action); 130 | expect(actual).toEqual(expected); 131 | }); 132 | it('should edit task', () => { 133 | const expected = getExpectedState({ 134 | 1: { 135 | task: 'A nice task', 136 | start: now, 137 | end: later, 138 | estimation: 60, 139 | repetition: 3, 140 | done: false, 141 | id: 'b', 142 | }, 143 | }); 144 | const action = editTask(1, { task: 'A nice task' }); 145 | const actual = taskReducer(defaultState, action); 146 | expect(actual).toEqual(expected); 147 | }); 148 | it('should delete task', () => { 149 | const expected = getExpectedState({ 150 | 1: undefined, 151 | }).filter(state => state); 152 | const action = deleteTask(1); 153 | const actual = taskReducer(defaultState, action); 154 | expect(actual).toEqual(expected); 155 | }); 156 | -------------------------------------------------------------------------------- /src/reducers/appProperties.js: -------------------------------------------------------------------------------- 1 | const appPropertiesReducer = (state = {}, action) => { 2 | switch (action.type) { 3 | case 'TOGGLE_NOT_YET': 4 | return { 5 | ...state, 6 | showNotYetTasks: action.flag, 7 | }; 8 | case 'TOGGLE_FULLSCREEN': 9 | return { 10 | ...state, 11 | fullscreen: action.isFullscreen, 12 | }; 13 | case 'CHANGE_CALENDAR_SYSTEM': 14 | return { 15 | ...state, 16 | calendarSystem: action.calendarSystem, 17 | }; 18 | case 'CHANGE_FIRST_DAY_OF_WEEK': 19 | return { 20 | ...state, 21 | firstDayOfWeek: action.firstDayOfWeek, 22 | }; 23 | case 'CHANGE_STARTUP_TAB': 24 | return { 25 | ...state, 26 | startupTab: action.startupTab, 27 | }; 28 | default: 29 | return state; 30 | } 31 | }; 32 | 33 | 34 | export default appPropertiesReducer; 35 | -------------------------------------------------------------------------------- /src/reducers/appUI.js: -------------------------------------------------------------------------------- 1 | const appPropertiesReducer = (state = {}, action) => { 2 | switch (action.type) { 3 | case 'RAISE_FAB': 4 | return { 5 | ...state, 6 | FABRaised: true, 7 | }; 8 | case 'LOWER_FAB': 9 | return { 10 | ...state, 11 | FABRaised: false, 12 | }; 13 | case 'CHANGE_TAB': 14 | return { 15 | ...state, 16 | currentTab: action.tab, 17 | }; 18 | default: 19 | return state; 20 | } 21 | }; 22 | 23 | 24 | export default appPropertiesReducer; 25 | -------------------------------------------------------------------------------- /src/reducers/idea.js: -------------------------------------------------------------------------------- 1 | const ideaReducer = (state = [], action) => { 2 | switch (action.type) { 3 | case 'ADD_IDEA': 4 | return [ 5 | ...state, 6 | action.idea, 7 | ]; 8 | case 'EDIT_IDEA': 9 | return [ 10 | ...state.slice(0, action.index), 11 | { 12 | ...state[action.index], 13 | idea: action.newIdea.idea, 14 | }, 15 | ...state.slice(action.index + 1), 16 | ]; 17 | case 'DELETE_IDEA': 18 | return [ 19 | ...state.slice(0, action.index), 20 | ...state.slice(action.index + 1), 21 | ]; 22 | default: 23 | return state; 24 | } 25 | }; 26 | 27 | export default ideaReducer; 28 | -------------------------------------------------------------------------------- /src/reducers/task.js: -------------------------------------------------------------------------------- 1 | import { addDays } from '../lib/date'; 2 | 3 | const taskReducer = (state = [], action) => { 4 | switch (action.type) { 5 | case 'ADD_TASK': 6 | return [ 7 | ...state, 8 | action.task, 9 | ]; 10 | case 'DO_TASK': 11 | if (state[action.index].repetition) { 12 | const task = state[action.index]; 13 | return [ 14 | ...state.slice(0, action.index), 15 | { 16 | ...state[action.index], 17 | start: addDays(task.repetition, task.start), 18 | end: addDays(task.repetition, task.end), 19 | }, 20 | ...state.slice(action.index + 1), 21 | ]; 22 | } 23 | return [ 24 | ...state.slice(0, action.index), 25 | { 26 | ...state[action.index], 27 | done: true, 28 | }, 29 | ...state.slice(action.index + 1), 30 | ]; 31 | case 'EDIT_TASK': 32 | return [ 33 | ...state.slice(0, action.index), 34 | { 35 | ...state[action.index], 36 | task: action.newTask.task, 37 | }, 38 | ...state.slice(action.index + 1), 39 | ]; 40 | case 'DELETE_TASK': 41 | return [ 42 | ...state.slice(0, action.index), 43 | ...state.slice(action.index + 1), 44 | ]; 45 | default: 46 | return state; 47 | } 48 | }; 49 | 50 | export default taskReducer; 51 | -------------------------------------------------------------------------------- /src/setupTests.js: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line 2 | import { configure } from 'enzyme'; 3 | // eslint-disable-next-line 4 | import Adapter from 'enzyme-adapter-react-16'; 5 | 6 | configure({ adapter: new Adapter() }); 7 | -------------------------------------------------------------------------------- /src/store.js: -------------------------------------------------------------------------------- 1 | import { createStore } from 'redux'; 2 | import rootReducer from './reducer'; 3 | 4 | import { fetchDatabaseState } from './lib/database'; 5 | 6 | const initialStateFactory = () => ({ 7 | tasks: [], 8 | ideas: [], 9 | appProperties: { 10 | showNotYetTasks: true, 11 | fullscreen: false, 12 | calendarSystem: 'en-US', 13 | firstDayOfWeek: 1, 14 | startupTab: 'tasks', 15 | }, 16 | appUI: { 17 | FABRaised: false, 18 | currentTab: 'tasks', 19 | }, 20 | }); 21 | 22 | const fetchState = () => fetchDatabaseState() || initialStateFactory(); 23 | 24 | const defaultState = fetchState(); 25 | 26 | const store = createStore(rootReducer, defaultState); 27 | 28 | export default store; 29 | --------------------------------------------------------------------------------