diff --git a/.babelrc b/.babelrc new file mode 100644 index 0000000..6647a85 --- /dev/null +++ b/.babelrc @@ -0,0 +1,37 @@ +{ + "presets": [ + "es2015", + "stage-0", + "react" + ], + "plugins": [ + "add-module-exports" + ], + "env": { + "production": { + "presets": [ + "react-optimize" + ], + "plugins": [ + "babel-plugin-dev-expression" + ] + }, + "development": { + "presets": [ + "react-hmre" + ] + }, + "test": { + "plugins": [ + [ + "webpack-loaders", + { + "config": "webpack.config.test.js", + "verbose": false + }, + "transform-flow-strip-types" + ] + ] + } + } +} diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..779f99a --- /dev/null +++ b/.editorconfig @@ -0,0 +1,12 @@ +root = true + +[*] +indent_style = space +indent_size = 4 +end_of_line = lf +charset = utf-8 +trim_trailing_whitespace = true +insert_final_newline = true + +[*.md] +trim_trailing_whitespace = false diff --git a/.eslintignore b/.eslintignore new file mode 100644 index 0000000..a9b203a --- /dev/null +++ b/.eslintignore @@ -0,0 +1 @@ +main.js diff --git a/.eslintrc b/.eslintrc new file mode 100644 index 0000000..d930936 --- /dev/null +++ b/.eslintrc @@ -0,0 +1,50 @@ +{ + "parser": "babel-eslint", + "extends": "airbnb", + "env": { + "browser": true, + "mocha": true, + "node": true + }, + "rules": { + "arrow-parens": [ + "error", + "as-needed" + ], + "consistent-return": "off", + "comma-dangle": "off", + "no-use-before-define": "off", + "import/no-unresolved": [ + "error", + { + "ignore": [ + "electron" + ] + } + ], + "import/no-extraneous-dependencies": "off", + "react/jsx-no-bind": "off", + "react/jsx-filename-extension": [ + "error", + { + "extensions": [ + ".js", + ".jsx" + ] + } + ], + "react/prefer-stateless-function": "off", + "generator-star-spacing": "off" + }, + "plugins": [ + "import", + "react" + ], + "settings": { + "import/resolver": { + "webpack": { + "config": "webpack.config.eslint.js" + } + } + } +} diff --git a/.flowconfig b/.flowconfig new file mode 100644 index 0000000..e69de29 diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..176a458 --- /dev/null +++ b/.gitattributes @@ -0,0 +1 @@ +* text=auto diff --git a/.gitconfig b/.gitconfig new file mode 100644 index 0000000..1d892a3 --- /dev/null +++ b/.gitconfig @@ -0,0 +1,3 @@ +[filter "tabspace"] + smudge = unexpand --tabs=4 --first-only + clean = expand --tabs=4 --initial diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..7946ebc --- /dev/null +++ b/.gitignore @@ -0,0 +1,43 @@ +# IDE +.idea + +# Logs +logs +*.log + +# Runtime data +pids +*.pid +*.seed + +# Directory for instrumented libs generated by jscoverage/JSCover +lib-cov + +# Coverage directory used by tools like istanbul +coverage + +# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) +.grunt + +# node-waf configuration +.lock-wscript + +# Compiled binary addons (http://nodejs.org/api/addons.html) +build/Release + +# Dependency directory +# https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git +node_modules + +# OSX +.DS_Store + +# App packaged +dist +release +main.js +main.js.map +Tempora-* + +# VsCode +.vscode/settings.json diff --git a/LICENSE.md b/LICENSE.md new file mode 100644 index 0000000..6bb5339 --- /dev/null +++ b/LICENSE.md @@ -0,0 +1,661 @@ +GNU AFFERO GENERAL PUBLIC LICENSE + Version 3, 19 November 2007 + +Copyright (C) 2007 Free Software Foundation, Inc. +Everyone is permitted to copy and distribute verbatim copies +of this license document, but changing it is not allowed. + + Preamble + +The GNU Affero General Public License is a free, copyleft license for +software and other kinds of works, specifically designed to ensure +cooperation with the community in the case of network server software. + +The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +our General Public Licenses are intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. + +When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + +Developers that use our General Public Licenses protect your rights +with two steps: (1) assert copyright on the software, and (2) offer +you this License which gives you legal permission to copy, distribute +and/or modify the software. + +A secondary benefit of defending all users' freedom is that +improvements made in alternate versions of the program, if they +receive widespread use, become available for other developers to +incorporate. Many developers of free software are heartened and +encouraged by the resulting cooperation. However, in the case of +software used on network servers, this result may fail to come about. +The GNU General Public License permits making a modified version and +letting the public access it on a server without ever releasing its +source code to the public. + +The GNU Affero General Public License is designed specifically to +ensure that, in such cases, the modified source code becomes available +to the community. It requires the operator of a network server to +provide the source code of the modified version running there to the +users of that server. Therefore, public use of a modified version, on +a publicly accessible server, gives the public access to the source +code of the modified version. + +An older license, called the Affero General Public License and +published by Affero, was designed to accomplish similar goals. This is +a different license, not a version of the Affero GPL, but Affero has +released a new version of the Affero GPL which permits relicensing under +this license. + +The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + +0. Definitions. + +"This License" refers to version 3 of the GNU Affero General Public License. + +"Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + +"The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + +To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + +A "covered work" means either the unmodified Program or a work based +on the Program. + +To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + +To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + +An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + +1. Source Code. + +The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + +A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + +The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + +The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + +The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + +The Corresponding Source for a work in source code form is that +same work. + +2. Basic Permissions. + +All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + +You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + +Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + +3. Protecting Users' Legal Rights From Anti-Circumvention Law. + +No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + +When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + +4. Conveying Verbatim Copies. + +You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + +You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + +5. Conveying Modified Source Versions. + +You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + +a) The work must carry prominent notices stating that you modified +it, and giving a relevant date. + +b) The work must carry prominent notices stating that it is +released under this License and any conditions added under section +7. This requirement modifies the requirement in section 4 to +"keep intact all notices". + +c) You must license the entire work, as a whole, under this +License to anyone who comes into possession of a copy. This +License will therefore apply, along with any applicable section 7 +additional terms, to the whole of the work, and all its parts, +regardless of how they are packaged. This License gives no +permission to license the work in any other way, but it does not +invalidate such permission if you have separately received it. + +d) If the work has interactive user interfaces, each must display +Appropriate Legal Notices; however, if the Program has interactive +interfaces that do not display Appropriate Legal Notices, your +work need not make them do so. + +A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + +6. Conveying Non-Source Forms. + +You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + +a) Convey the object code in, or embodied in, a physical product +(including a physical distribution medium), accompanied by the +Corresponding Source fixed on a durable physical medium +customarily used for software interchange. + +b) Convey the object code in, or embodied in, a physical product +(including a physical distribution medium), accompanied by a +written offer, valid for at least three years and valid for as +long as you offer spare parts or customer support for that product +model, to give anyone who possesses the object code either (1) a +copy of the Corresponding Source for all the software in the +product that is covered by this License, on a durable physical +medium customarily used for software interchange, for a price no +more than your reasonable cost of physically performing this +conveying of source, or (2) access to copy the +Corresponding Source from a network server at no charge. + +c) Convey individual copies of the object code with a copy of the +written offer to provide the Corresponding Source. This +alternative is allowed only occasionally and noncommercially, and +only if you received the object code with such an offer, in accord +with subsection 6b. + +d) Convey the object code by offering access from a designated +place (gratis or for a charge), and offer equivalent access to the +Corresponding Source in the same way through the same place at no +further charge. You need not require recipients to copy the +Corresponding Source along with the object code. If the place to +copy the object code is a network server, the Corresponding Source +may be on a different server (operated by you or a third party) +that supports equivalent copying facilities, provided you maintain +clear directions next to the object code saying where to find the +Corresponding Source. Regardless of what server hosts the +Corresponding Source, you remain obligated to ensure that it is +available for as long as needed to satisfy these requirements. + +e) Convey the object code using peer-to-peer transmission, provided +you inform other peers where the object code and Corresponding +Source of the work are being offered to the general public at no +charge under subsection 6d. + +A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + +A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + +"Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + +If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + +The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + +Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + +7. Additional Terms. + +"Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + +When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + +Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + +a) Disclaiming warranty or limiting liability differently from the +terms of sections 15 and 16 of this License; or + +b) Requiring preservation of specified reasonable legal notices or +author attributions in that material or in the Appropriate Legal +Notices displayed by works containing it; or + +c) Prohibiting misrepresentation of the origin of that material, or +requiring that modified versions of such material be marked in +reasonable ways as different from the original version; or + +d) Limiting the use for publicity purposes of names of licensors or +authors of the material; or + +e) Declining to grant rights under trademark law for use of some +trade names, trademarks, or service marks; or + +f) Requiring indemnification of licensors and authors of that +material by anyone who conveys the material (or modified versions of +it) with contractual assumptions of liability to the recipient, for +any liability that these contractual assumptions directly impose on +those licensors and authors. + +All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + +If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + +Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + +8. Termination. + +You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + +However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + +Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + +Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + +9. Acceptance Not Required for Having Copies. + +You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + +10. Automatic Licensing of Downstream Recipients. + +Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + +An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + +You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + +11. Patents. + +A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + +A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + +Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + +In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + +If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + +If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + +A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + +Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + +12. No Surrender of Others' Freedom. + +If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + +13. Remote Network Interaction; Use with the GNU General Public License. + +Notwithstanding any other provision of this License, if you modify the +Program, your modified version must prominently offer all users +interacting with it remotely through a computer network (if your version +supports such interaction) an opportunity to receive the Corresponding +Source of your version by providing access to the Corresponding Source +from a network server at no charge, through some standard or customary +means of facilitating copying of software. This Corresponding Source +shall include the Corresponding Source for any work covered by version 3 +of the GNU General Public License that is incorporated pursuant to the +following paragraph. + +Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the work with which it is combined will remain governed by version +3 of the GNU General Public License. + +14. Revised Versions of this License. + +The Free Software Foundation may publish revised and/or new versions of +the GNU Affero General Public License from time to time. Such new versions +will be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + +Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU Affero General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU Affero General Public License, you may choose any version ever published +by the Free Software Foundation. + +If the Program specifies that a proxy can decide which future +versions of the GNU Affero General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + +Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + +15. Disclaimer of Warranty. + +THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + +16. Limitation of Liability. + +IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + +17. Interpretation of Sections 15 and 16. + +If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + +How to Apply These Terms to Your New Programs + +If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + +To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + +Copyright (C) + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published +by the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + +If your software can interact with users remotely through a computer +network, you should also make sure that it provides a way for users to +get its source. For example, if your program is a web application, its +interface could display a "Source" link that leads users to an archive +of the code. There are many ways you could offer source, and different +solutions will be better for different programs; see section 13 for the +specific requirements. + +You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU AGPL, see +. diff --git a/README.md b/README.md new file mode 100644 index 0000000..ba75895 --- /dev/null +++ b/README.md @@ -0,0 +1,55 @@ +# Tempora + +Combining Tempo and Jira in one single app. + +## How to install + +```bash +$ npm install +``` + +## How to run + +```bash +$ npm run dev +``` + +*Note: requires a node version >= 4 and an npm version >= 2.* + +## How to package +```bash +$ npm run build +$ npm run package +``` + +To package apps for all platforms: +```bash +$ npm run package -- --all +``` + +To package apps with options: +```bash +$ npm run package -- --[option] +``` + +### Options +- --name, -n: Application name (default: ElectronReact) +- --version, -v: Electron version (default: latest version) +- --asar, -a: [asar](https://github.com/atom/asar) support (default: false) +- --icon, -i: Application icon +- --all: pack for all platforms + +Use `electron-packager` to pack your app with `--all` options for darwin (osx), linux and win32 (windows) platform. After build, you will find them in `release` folder. Otherwise, you will only find one for your os. + +`test`, `tools`, `release` folder and devDependencies in `package.json` will be ignored by default. + +### Default Ignore modules +We add some module's `peerDependencies` to ignore option as default for application size reduction. + +- `babel-core` is required by `babel-loader` and its size is ~19 MB +- `node-libs-browser` is required by `webpack` and its size is ~3MB. + +> **Note:** If you want to use any above modules in runtime, for example: `require('babel/register')`, you should move them from `devDependencies` to `dependencies`. + +### Building windows apps from non-windows platforms +Please checkout [Building windows apps from non-windows platforms](https://github.com/maxogden/electron-packager#building-windows-apps-from-non-windows-platforms). diff --git a/app/app.html b/app/app.html new file mode 100644 index 0000000..2924f3d --- /dev/null +++ b/app/app.html @@ -0,0 +1,31 @@ + + + + + Tempora + + + + + + + +
+ + + + diff --git a/app/app.icns b/app/app.icns new file mode 100644 index 0000000..42f06cd Binary files /dev/null and b/app/app.icns differ diff --git a/app/app.ico b/app/app.ico new file mode 100644 index 0000000..dfa74c8 Binary files /dev/null and b/app/app.ico differ diff --git a/app/app.png b/app/app.png new file mode 100644 index 0000000..b2c0e5a Binary files /dev/null and b/app/app.png differ diff --git a/app/assets/icons/logo.png b/app/assets/icons/logo.png new file mode 100644 index 0000000..8e6315b Binary files /dev/null and b/app/assets/icons/logo.png differ diff --git a/app/assets/images/logo.png b/app/assets/images/logo.png new file mode 100644 index 0000000..cfd7185 Binary files /dev/null and b/app/assets/images/logo.png differ diff --git a/app/assets/scss/base/_page.scss b/app/assets/scss/base/_page.scss new file mode 100644 index 0000000..b9b6d76 --- /dev/null +++ b/app/assets/scss/base/_page.scss @@ -0,0 +1,51 @@ +/** + * PAGE + * base/page.scss + */ + +* { + box-sizing: border-box; +} + +html { + font-family: $f-primary; + color: $c-text; + -ms-text-size-adjust: 100%; + -webkit-text-size-adjust: 100%; + -moz-osx-font-smoothing: grayscale; + -webkit-font-smoothing: antialiased; + + min-height: 100%; + overflow-x: hidden; +} + +body { + background-color: #fff; + color: $c-text; + + height: 100vh; + margin: 0; + padding: 0; + overflow-y: hidden; + + position: relative; + + -webkit-app-region: drag; +} + +a, +button, +input, +select, +textarea, +.no-drag { + -webkit-app-region: no-drag; +} + +#root { + height: 100%; +} + +::-webkit-scrollbar { + display: none; +} diff --git a/app/assets/scss/components/_datepicker.scss b/app/assets/scss/components/_datepicker.scss new file mode 100644 index 0000000..7167949 --- /dev/null +++ b/app/assets/scss/components/_datepicker.scss @@ -0,0 +1,427 @@ +/** +* DATEPICKER +* component/datepicker.scss +*/ + +.react-datepicker__tether-element-attached-top .react-datepicker__triangle, +.react-datepicker__tether-element-attached-bottom .react-datepicker__triangle, +.react-datepicker__year-read-view--down-arrow { + margin-left: -8px; + + position: absolute; +} + +.react-datepicker__tether-element-attached-top .react-datepicker__triangle, +.react-datepicker__tether-element-attached-bottom .react-datepicker__triangle, +.react-datepicker__year-read-view--down-arrow, +.react-datepicker__tether-element-attached-top .react-datepicker__triangle::before, +.react-datepicker__tether-element-attached-bottom .react-datepicker__triangle::before, +.react-datepicker__year-read-view--down-arrow::before { + box-sizing: content-box; + border: 8px solid transparent; + height: 0; + width: 1px; + + position: absolute; +} + +.react-datepicker__tether-element-attached-top .react-datepicker__triangle::before, +.react-datepicker__tether-element-attached-bottom .react-datepicker__triangle::before, +.react-datepicker__year-read-view--down-arrow::before { + content: ""; + + border-width: 8px; + border-bottom-color: #aeaeae; + + z-index: -1; + left: -8px; +} + +.react-datepicker__tether-element-attached-top .react-datepicker__triangle { + margin-top: -8px; + + top: 0; +} + +.react-datepicker__tether-element-attached-top .react-datepicker__triangle, +.react-datepicker__tether-element-attached-top .react-datepicker__triangle::before { + border-top: none; + border-bottom-color: #f0f0f0; +} + +.react-datepicker__tether-element-attached-top .react-datepicker__triangle::before { + border-bottom-color: #aeaeae; + + top: -1px; +} + +.react-datepicker__tether-element-attached-bottom .react-datepicker__triangle, +.react-datepicker__year-read-view--down-arrow { + margin-bottom: -8px; + + bottom: 0; +} + +.react-datepicker__tether-element-attached-bottom .react-datepicker__triangle, +.react-datepicker__year-read-view--down-arrow, +.react-datepicker__tether-element-attached-bottom .react-datepicker__triangle::before, +.react-datepicker__year-read-view--down-arrow::before { + border-bottom: none; + border-top-color: #fff; +} + +.react-datepicker__tether-element-attached-bottom .react-datepicker__triangle::before, +.react-datepicker__year-read-view--down-arrow::before { + border-top-color: #aeaeae; + + bottom: -1px; +} + +.react-datepicker { + font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; + font-size: .8rem; + background-color: #fff; + color: #000; + + border: 1px solid #aeaeae; + border-radius: .3rem; + display: inline-block; + + position: relative; +} + +.react-datepicker__triangle { + position: absolute; + left: 50px; +} + +.react-datepicker__tether-element-attached-bottom.react-datepicker__tether-element { + margin-top: -20px; +} + +.react-datepicker__header { + text-align: center; + background-color: #f0f0f0; + + border-bottom: 1px solid #aeaeae; + border-top-left-radius: .3rem; + border-top-right-radius: .3rem; + padding-top: 8px; + + position: relative; +} + +.react-datepicker__header__dropdown--select { + margin-top: -16px; +} + +.react-datepicker__year-dropdown-container--select, +.react-datepicker__month-dropdown-container--select { + display: inline-block; + margin: 0 2px; +} + +.react-datepicker__current-month { + color: #000; + font-weight: bold; + font-size: .944rem; + + margin-top: 0; +} + +.react-datepicker__current-month--hasYearDropdown { + margin-bottom: 16px; +} + +.react-datepicker__navigation { + line-height: 1.7rem; + text-align: center; + + width: 0; + border: .45rem solid transparent; + + position: absolute; + top: 10px; + + cursor: pointer; +} + +.react-datepicker__navigation--previous { + border-right-color: #ccc; + + left: 10px; +} + +.react-datepicker__navigation--previous:hover { + border-right-color: #b3b3b3; +} + +.react-datepicker__navigation--next { + border-left-color: #ccc; + + right: 10px; +} + +.react-datepicker__navigation--next:hover { + border-left-color: #b3b3b3; +} + +.react-datepicker__navigation--years { + display: block; + margin-left: auto; + margin-right: auto; + + position: relative; + top: 0; +} + +.react-datepicker__navigation--years-previous { + border-top-color: #ccc; + + top: 4px; +} + +.react-datepicker__navigation--years-previous:hover { + border-top-color: #b3b3b3; +} + +.react-datepicker__navigation--years-upcoming { + border-bottom-color: #ccc; + + top: -4px; +} + +.react-datepicker__navigation--years-upcoming:hover { + border-bottom-color: #b3b3b3; +} + +.react-datepicker__month { + text-align: center; + + margin: .4rem; +} + +.react-datepicker__day-name, +.react-datepicker__day { + color: #000; + line-height: 1.7rem; + text-align: center; + + margin: .166rem; + display: inline-block; + width: 1.7rem; +} + +.react-datepicker__day { + cursor: pointer; +} + +.react-datepicker__day:hover { + background-color: #f0f0f0; + + border-radius: .3rem; +} + +.react-datepicker__day--today { + font-weight: bold; +} + +.react-datepicker__day--highlighted { + background-color: #3dcc4a; + color: #fff; + + border-radius: .3rem; +} + +.react-datepicker__day--highlighted:hover { + background-color: #32be3f; +} + +.react-datepicker__day--selected, +.react-datepicker__day--in-selecting-range, +.react-datepicker__day--in-range { + background-color: #216ba5; + color: #fff; + + border-radius: .3rem; +} + +.react-datepicker__day--selected:hover, +.react-datepicker__day--in-selecting-range:hover, +.react-datepicker__day--in-range:hover { + background-color: #1d5d90; +} + +.react-datepicker__day--in-selecting-range:not(.react-datepicker__day--in-range) { + background-color: rgba(33, 107, 165, .5); +} + +.react-datepicker__month--selecting-range .react-datepicker__day--in-range:not(.react-datepicker__day--in-selecting-range) { + background-color: #f0f0f0; + color: #000; +} + +.react-datepicker__day--disabled { + color: #ccc; + + cursor: default; +} + +.react-datepicker__day--disabled:hover { + background-color: transparent; +} + +.react-datepicker__input-container { + display: inline-block; + width: 100%; + + position: relative; +} + +.react-datepicker__year-read-view { + width: 50%; + border: 1px solid transparent; + border-radius: .3rem; + + left: 25%; + position: absolute; + bottom: 25px; +} + +.react-datepicker__year-read-view:hover { + cursor: pointer; +} + +.react-datepicker__year-read-view:hover .react-datepicker__year-read-view--down-arrow { + border-top-color: #b3b3b3; +} + +.react-datepicker__year-read-view--down-arrow { + border-top-color: #ccc; + border-width: .45rem; + margin-bottom: 3px; + + left: 5px; + top: 9px; + position: relative; +} + +.react-datepicker__year-read-view--selected-year { + right: .45rem; + position: relative; +} + +.react-datepicker__year-dropdown { + background-color: #f0f0f0; + text-align: center; + + width: 50%; + border-radius: .3rem; + border: 1px solid #aeaeae; + + position: absolute; + left: 25%; + top: 30px; +} + +.react-datepicker__year-dropdown:hover { + cursor: pointer; +} + +.react-datepicker__year-dropdown--scrollable { + height: 150px; + overflow-y: scroll; +} + +.react-datepicker__year-option { + line-height: 20px; + + width: 100%; + display: block; + margin-left: auto; + margin-right: auto; +} + +.react-datepicker__year-option:first-of-type { + border-top-left-radius: .3rem; + border-top-right-radius: .3rem; +} + +.react-datepicker__year-option:last-of-type { + border-bottom-left-radius: .3rem; + border-bottom-right-radius: .3rem; + + -moz-user-select: none; + -ms-user-select: none; + -webkit-user-select: none; + user-select: none; +} + +.react-datepicker__year-option:hover { + background-color: #ccc; +} + +.react-datepicker__year-option:hover .react-datepicker__navigation--years-upcoming { + border-bottom-color: #b3b3b3; +} + +.react-datepicker__year-option:hover .react-datepicker__navigation--years-previous { + border-top-color: #b3b3b3; +} + +.react-datepicker__year-option--selected { + position: absolute; + left: 30px; +} + +.react-datepicker__close-icon { + background-color: transparent; + + border: 0; + display: inline-block; + vertical-align: middle; + height: 0; + outline: 0; + padding: 0; + + cursor: pointer; +} + +.react-datepicker__close-icon::after { + content: "\00d7"; + + background-color: #216ba5; + color: #fff; + text-align: center; + line-height: 1; + font-size: 12px; + + box-sizing: border-box; + border-radius: 50%; + margin: -8px auto 0; + padding: 2px; + height: 16px; + width: 16px; + + bottom: 0; + position: absolute; + right: 7px; + top: 50%; + + cursor: pointer; +} + +.react-datepicker__today-button { + background: #f0f0f0; + text-align: center; + font-weight: bold; + + border-top: 1px solid #aeaeae; + padding: 5px 0; + + cursor: pointer; +} + +.react-datepicker__tether-element { + z-index: 2147483647; +} diff --git a/app/assets/scss/components/_layout.scss b/app/assets/scss/components/_layout.scss new file mode 100644 index 0000000..28c03f4 --- /dev/null +++ b/app/assets/scss/components/_layout.scss @@ -0,0 +1,58 @@ +/** + * LAYOUT + * component/layout.scss + */ + +.container { + padding: 20px; +} + +.layout { + display: flex; + width: 100%; + height: 100%; + + &__aside { + color: #d7dadb; + background: $c-primary--dark; /* Old browsers */ + background: -moz-linear-gradient(left, $c-primary--dark 93%, #243342 100%); /* FF3.6-15 */ + background: -webkit-linear-gradient(left, $c-primary--dark 93%, #243342 100%); /* Chrome10-25,Safari5.1-6 */ + background: linear-gradient(to right, $c-primary--dark 93%, #243342 100%); /* W3C, IE10+, FF16+, Chrome26+, Opera12+, Safari7+ */ + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr="#2c3e50", endColorstr="#243342", GradientType=1); /* IE6-9 */ + + display: flex; + flex-direction: column; + flex: 0 0 300px; + } + + &__content { + color: #000; + background-color: #fff; + + overflow: scroll; + + flex: 1; + } +} + +.row { + margin-bottom: 1em; +} + +.col { + vertical-align: top; + + display: inline-block; + + &--middle { + vertical-align: middle; + } + + &--1-3 { + width: 33.33%; + } + + &--2-3 { + width: 66.66%; + } +} diff --git a/app/assets/scss/components/_page.scss b/app/assets/scss/components/_page.scss new file mode 100644 index 0000000..cfa3876 --- /dev/null +++ b/app/assets/scss/components/_page.scss @@ -0,0 +1,126 @@ +/** + * PAGE + * component/page.scss + */ + +.page { + background-color: #fff; + color: #000; + + padding: 20px; + + &__close { + font-size: 20px; + text-decoration: none; + color: #000; + + position: absolute; + top: -40px; + right: 0; + } + + &__header { + position: relative; + } + + &__title { + font-weight: 400; + + margin: 47px 0 20px; + } + + &__subtitle { + font-weight: 700; + text-transform: uppercase; + font-size: 14px; + + margin: 0 0 15px; + } + + &__search { + width: 300px; + padding: 10px; + border: 1px solid #ddd; + border-radius: 100px; + + position: absolute; + top: 2px; + right: 0; + + &:focus { + border-color: #000; + + outline: 0; + } + } + + &__items { + margin: 10px 0 30px; + padding: 0; + + list-style-type: none; + } + + &__item { + &__link { + color: $c-text; + text-decoration: none; + + padding: 15px 0; + display: block; + border-bottom: 1px solid $c-neutral; + + &:hover { + color: $c-primary; + + border-bottom-color: $c-primary; + } + + &--with-icon { + display: inline-block; + width: calc(100% - 40px); + } + + &__desc { + font-size: 90%; + + display: inline-block; + margin-left: 10px; + + opacity: .85; + } + } + + &__link-icon { + text-align: right; + color: $c-primary; + + display: inline-block; + width: 40px; + margin-top: 16px; + vertical-align: top; + + &:hover { + color: $c-secondary; + } + } + } + + &__sep { + background: $c-neutral; + + display: block; + width: 100%; + height: 1px; + margin: 20px 0; + border: 0; + } + + &__notfound { + text-align: center; + font-style: italic; + color: #666; + + padding: 40px 0; + } +} diff --git a/app/assets/scss/components/_timepicker.scss b/app/assets/scss/components/_timepicker.scss new file mode 100644 index 0000000..b67f774 --- /dev/null +++ b/app/assets/scss/components/_timepicker.scss @@ -0,0 +1,182 @@ +/** +* TIMEPICKER +* component/timepicker.scss +*/ + +.rc-time-picker { + display: inline-block; + box-sizing: border-box; +} + +.rc-time-picker * { + box-sizing: border-box; +} + +.rc-time-picker-input { + background: none; + + width: 170px; + border: 0; +} + +.rc-time-picker-panel { + width: 170px; + box-sizing: border-box; + + position: absolute; + z-index: 1070; +} + +.rc-time-picker-panel * { + box-sizing: border-box; +} + +.rc-time-picker-panel-inner { + font-size: 12px; + text-align: left; + background-color: #fff; + background-clip: padding-box; + line-height: 1.5; + + display: inline-block; + outline: none; + border-radius: 3px; + border: 1px solid #ccc; + + position: relative; + + box-shadow: 0 1px 5px #ccc; + + list-style: none; +} + +.rc-time-picker-panel-input { + line-height: 1.5; + + margin: 0; + padding: 0; + width: 100%; + outline: 0; + border: 1px solid transparent; + + cursor: auto; +} + +.rc-time-picker-panel-input-wrap { + box-sizing: border-box; + padding: 6px; + border-bottom: 1px solid #e9e9e9; + + position: relative; +} + +.rc-time-picker-panel-input-invalid { + border-color: red; +} + +.rc-time-picker-panel-clear-btn { + text-align: center; + line-height: 20px; + + overflow: hidden; + width: 20px; + height: 20px; + margin: 0; + + position: absolute; + right: 6px; + top: 6px; + + cursor: pointer; +} + +.rc-time-picker-panel-clear-btn::after { + content: "x"; + + font-size: 12px; + color: #aaa; + line-height: 1; + + display: inline-block; + width: 20px; + + -webkit-transition: color .3s ease; + transition: color .3s ease; +} + +.rc-time-picker-panel-clear-btn:hover::after { + color: #666; +} + +.rc-time-picker-panel-select { + font-size: 12px; + + border: 1px solid #e9e9e9; + border-width: 0 1px; + margin-left: -1px; + box-sizing: border-box; + width: 56px; + overflow: hidden; + + float: left; + position: relative; +} + +.rc-time-picker-panel-select:hover { + overflow-y: auto; +} + +.rc-time-picker-panel-select:first-child { + border-left: 0; + margin-left: 0; +} + +.rc-time-picker-panel-select:last-child { + border-right: 0; +} + +.rc-time-picker-panel-select ul { + box-sizing: border-box; + margin: 0; + padding: 0; + width: 100%; + max-height: 144px; + + list-style: none; +} + +.rc-time-picker-panel-select li { + line-height: 24px; + text-align: left; + + box-sizing: content-box; + margin: 0; + padding: 0 0 0 16px; + width: 100%; + height: 24px; + + cursor: pointer; + list-style: none; + -moz-user-select: none; + -ms-user-select: none; + -webkit-user-select: none; + user-select: none; +} + +.rc-time-picker-panel-select li:hover { + background: #edfaff; +} + +li.rc-time-picker-panel-select-option-selected { + background: #edfaff; + color: #2db7f5; +} + +li.rc-time-picker-panel-select-option-disabled { + color: #bfbfbf; +} + +li.rc-time-picker-panel-select-option-disabled:hover { + background: transparent; + cursor: not-allowed; +} diff --git a/app/assets/scss/generic/_normalize.scss b/app/assets/scss/generic/_normalize.scss new file mode 100644 index 0000000..8c811df --- /dev/null +++ b/app/assets/scss/generic/_normalize.scss @@ -0,0 +1,424 @@ +/*! normalize.css v3.0.3 | MIT License | github.com/necolas/normalize.css */ + +// +// 1. Set default font family to sans-serif. +// 2. Prevent iOS and IE text size adjust after device orientation change, +// without disabling user zoom. +// + +html { + font-family: sans-serif; // 1 + -ms-text-size-adjust: 100%; // 2 + -webkit-text-size-adjust: 100%; // 2 +} + +// +// Remove default margin. +// + +body { + margin: 0; +} + +// HTML5 display definitions +// ========================================================================== + +// +// Correct `block` display not defined for any HTML5 element in IE 8/9. +// Correct `block` display not defined for `details` or `summary` in IE 10/11 +// and Firefox. +// Correct `block` display not defined for `main` in IE 11. +// + +article, +aside, +details, +figcaption, +figure, +footer, +header, +hgroup, +main, +menu, +nav, +section, +summary { + display: block; +} + +// +// 1. Correct `inline-block` display not defined in IE 8/9. +// 2. Normalize vertical alignment of `progress` in Chrome, Firefox, and Opera. +// + +audio, +canvas, +progress, +video { + display: inline-block; // 1 + vertical-align: baseline; // 2 +} + +// +// Prevent modern browsers from displaying `audio` without controls. +// Remove excess height in iOS 5 devices. +// + +audio:not([controls]) { + display: none; + height: 0; +} + +// +// Address `[hidden]` styling not present in IE 8/9/10. +// Hide the `template` element in IE 8/9/10/11, Safari, and Firefox < 22. +// + +[hidden], +template { + display: none; +} + +// Links +// ========================================================================== + +// +// Remove the gray background color from active links in IE 10. +// + +a { + background-color: transparent; +} + +// +// Improve readability of focused elements when they are also in an +// active/hover state. +// + +a:active, +a:hover { + outline: 0; +} + +// Text-level semantics +// ========================================================================== + +// +// Address styling not present in IE 8/9/10/11, Safari, and Chrome. +// + +abbr[title] { + border-bottom: 1px dotted; +} + +// +// Address style set to `bolder` in Firefox 4+, Safari, and Chrome. +// + +b, +strong { + font-weight: bold; +} + +// +// Address styling not present in Safari and Chrome. +// + +dfn { + font-style: italic; +} + +// +// Address variable `h1` font-size and margin within `section` and `article` +// contexts in Firefox 4+, Safari, and Chrome. +// + +h1 { + font-size: 2em; + margin: .67em 0; +} + +// +// Address styling not present in IE 8/9. +// + +mark { + background: #ff0; + color: #000; +} + +// +// Address inconsistent and variable font size in all browsers. +// + +small { + font-size: 80%; +} + +// +// Prevent `sub` and `sup` affecting `line-height` in all browsers. +// + +sub, +sup { + font-size: 75%; + line-height: 0; + vertical-align: baseline; + position: relative; +} + +sup { + top: -.5em; +} + +sub { + bottom: -.25em; +} + +// Embedded content +// ========================================================================== + +// +// Remove border when inside `a` element in IE 8/9/10. +// + +img { + border: 0; +} + +// +// Correct overflow not hidden in IE 9/10/11. +// + +svg:not(:root) { + overflow: hidden; +} + +// Grouping content +// ========================================================================== + +// +// Address margin not present in IE 8/9 and Safari. +// + +figure { + margin: 1em 40px; +} + +// +// Address differences between Firefox and other browsers. +// + +hr { + box-sizing: content-box; + height: 0; +} + +// +// Contain overflow in all browsers. +// + +pre { + overflow: auto; +} + +// +// Address odd `em`-unit font size rendering in all browsers. +// + +code, +kbd, +pre, +samp { + font-family: monospace, monospace; + font-size: 1em; +} + +// Forms +// ========================================================================== + +// +// Known limitation: by default, Chrome and Safari on OS X allow very limited +// styling of `select`, unless a `border` property is set. +// + +// +// 1. Correct color not being inherited. +// Known issue: affects color of disabled elements. +// 2. Correct font properties not being inherited. +// 3. Address margins set differently in Firefox 4+, Safari, and Chrome. +// + +button, +input, +optgroup, +select, +textarea { + color: inherit; // 1 + font: inherit; // 2 + margin: 0; // 3 +} + +// +// Address `overflow` set to `hidden` in IE 8/9/10/11. +// + +button { + overflow: visible; +} + +// +// Address inconsistent `text-transform` inheritance for `button` and `select`. +// All other form control elements do not inherit `text-transform` values. +// Correct `button` style inheritance in Firefox, IE 8/9/10/11, and Opera. +// Correct `select` style inheritance in Firefox. +// + +button, +select { + text-transform: none; +} + +// +// 1. Avoid the WebKit bug in Android 4.0.* where (2) destroys native `audio` +// and `video` controls. +// 2. Correct inability to style clickable `input` types in iOS. +// 3. Improve usability and consistency of cursor style between image-type +// `input` and others. +// + +button, +html input[type="button"], // 1 +input[type="reset"], +input[type="submit"] { + -webkit-appearance: button; // 2 + cursor: pointer; // 3 +} + +// +// Re-set default cursor for disabled elements. +// + +button[disabled], +html input[disabled] { + cursor: default; +} + +// +// Remove inner padding and border in Firefox 4+. +// + +button::-moz-focus-inner, +input::-moz-focus-inner { + border: 0; + padding: 0; +} + +// +// Address Firefox 4+ setting `line-height` on `input` using `!important` in +// the UA stylesheet. +// + +input { + line-height: normal; +} + +// +// It's recommended that you don't attempt to style these elements. +// Firefox's implementation doesn't respect box-sizing, padding, or width. +// +// 1. Address box sizing set to `content-box` in IE 8/9/10. +// 2. Remove excess padding in IE 8/9/10. +// + +input[type="checkbox"], +input[type="radio"] { + box-sizing: border-box; // 1 + padding: 0; // 2 +} + +// +// Fix the cursor style for Chrome's increment/decrement buttons. For certain +// `font-size` values of the `input`, it causes the cursor style of the +// decrement button to change from `default` to `text`. +// + +input[type="number"]::-webkit-inner-spin-button, +input[type="number"]::-webkit-outer-spin-button { + height: auto; +} + +// +// 1. Address `appearance` set to `searchfield` in Safari and Chrome. +// 2. Address `box-sizing` set to `border-box` in Safari and Chrome. +// + +input[type="search"] { + box-sizing: content-box; //2 + -webkit-appearance: textfield; // 1 +} + +// +// Remove inner padding and search cancel button in Safari and Chrome on OS X. +// Safari (but not Chrome) clips the cancel button when the search input has +// padding (and `textfield` appearance). +// + +input[type="search"]::-webkit-search-cancel-button, +input[type="search"]::-webkit-search-decoration { + -webkit-appearance: none; +} + +// +// Define consistent border, margin, and padding. +// + +fieldset { + border: 1px solid #c0c0c0; + margin: 0 2px; + padding: .35em .625em .75em; +} + +// +// 1. Correct `color` not being inherited in IE 8/9/10/11. +// 2. Remove padding so people aren't caught out if they zero out fieldsets. +// + +legend { + border: 0; // 1 + padding: 0; // 2 +} + +// +// Remove default vertical scrollbar in IE 8/9/10/11. +// + +textarea { + overflow: auto; +} + +// +// Don't inherit the `font-weight` (applied by a rule above). +// NOTE: the default cannot safely be changed in Chrome and Safari on OS X. +// + +optgroup { + font-weight: bold; +} + +// Tables +// ========================================================================== + +// +// Remove most spacing between table cells. +// + +table { + border-collapse: collapse; + border-spacing: 0; +} + +td, +th { + padding: 0; +} diff --git a/app/assets/scss/settings/_colors.scss b/app/assets/scss/settings/_colors.scss new file mode 100644 index 0000000..c89deeb --- /dev/null +++ b/app/assets/scss/settings/_colors.scss @@ -0,0 +1,12 @@ +/** + * COLORS + * settings/colors.scss + */ + +$c-primary: #6dbcdb; +$c-primary--dark: #2c3e50; +$c-secondary: #fc4349; + +$c-neutral: #ddd; + +$c-text: #333; diff --git a/app/assets/scss/settings/_typo.scss b/app/assets/scss/settings/_typo.scss new file mode 100644 index 0000000..79b723f --- /dev/null +++ b/app/assets/scss/settings/_typo.scss @@ -0,0 +1,6 @@ +/** + * TYPOGRAPHY + * settings/typo.scss + */ + +$f-primary: "Roboto", Arial, sans-serif; diff --git a/app/assets/scss/trumps/_helpers.scss b/app/assets/scss/trumps/_helpers.scss new file mode 100644 index 0000000..17dbba3 --- /dev/null +++ b/app/assets/scss/trumps/_helpers.scss @@ -0,0 +1,12 @@ +/** + * HELPERS + * trumps/helpers.scss + */ + +.is-centered { + text-align: center !important; +} + +.is-right { + text-align: right !important; +} diff --git a/app/components/app-modal/AppModal.js b/app/components/app-modal/AppModal.js new file mode 100644 index 0000000..28f970a --- /dev/null +++ b/app/components/app-modal/AppModal.js @@ -0,0 +1,51 @@ +import React, {Component} from 'react'; +import Modal from 'react-modal'; + +import './Modal.scss'; + +export default class AppModal extends Component { + constructor(props) { + super(props); + this.state = { + visible: false, + title: "", + content: "" + }; + + window.eventEmitter.addListener('requestModal', this.openModal.bind(this)); + } + + componentDidMount() { + Modal.setAppElement("#root"); + } + + openModal(title, content: 'undefined') { + this.setState({ + visible: true, + title: title, + content: content + }); + } + + closeModal() { + this.setState({ visible: false }); + } + + render() { + return ( + +

{this.state.title}

+

{this.state.content}

+
+ +
+
+ ); + } +} diff --git a/app/components/app-modal/Modal.scss b/app/components/app-modal/Modal.scss new file mode 100644 index 0000000..c40fb38 --- /dev/null +++ b/app/components/app-modal/Modal.scss @@ -0,0 +1,63 @@ +@import "../../assets/scss/settings/colors"; + +.modal { + background-color: #fff; + text-align: center; + + display: flex; + flex-direction: column; + justify-content: center; + border-radius: 3px; + + position: absolute; + top: 30%; + left: 20%; + right: 20%; + bottom: 30%; + + -webkit-box-shadow: 0px 0px 10px 2px rgba(0, 0, 0, 0.1); + -moz-box-shadow: 0px 0px 10px 2px rgba(0, 0, 0, 0.1); + box-shadow: 0px 0px 10px 2px rgba(0, 0, 0, 0.1); + + &:focus { + outline: 0; + } + + &__title { + margin: 0 0 30px; + } + + &__content { + font-style: italic; + + margin: 0 0 20px; + } + + &__btn { + background: none; + + display: inline-block; + padding: 0 1px 5px; + border: 0; + border-bottom: 1px solid #000; + + &:hover, + &:focus { + color: $c-primary; + + border-bottom-color: $c-primary; + + outline: 0; + } + } +} + +.modal-overlay { + background-color: rgba(0, 0, 0, .8); + + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; +} diff --git a/app/components/dashboard/Dashboard.js b/app/components/dashboard/Dashboard.js new file mode 100644 index 0000000..ee61712 --- /dev/null +++ b/app/components/dashboard/Dashboard.js @@ -0,0 +1,90 @@ +import React, {Component, PropTypes} from "react"; +import StatsRecap from "../stats-recap/StatsRecap"; + +import * as WorklogService from "../../services/WorklogService"; + +import "./Dashboard.scss"; + +export default class Dashboard extends Component { + constructor(props) { + super(props); + + this.state = { + selectedWorklog: "" + } + + window.eventEmitter.addListener('unselectActiveWorklog', () => this.setState({ selectedWorklog: "" })); + } + + selectWorklog(entry) { + window.eventEmitter.emitEvent('selectWorklog', [entry]); + + this.setState({ + selectedWorklog: entry.id + }); + } + + showTimeframe(entries, entriesTitle) { + if (entries.length > 0) { + return ( + + + {entriesTitle} + + this.props.orderByTime(entries)}> + + + this.props.orderByName(entries)}> + + + + + {this.showEntries(entries)} + + ); + } + } + + showEntries(entries) { + return entries.map((entry: Worklog) => ( + this.selectWorklog(entry)} className={this.state.selectedWorklog == entry.id ? "is-active" : ""}> + {WorklogService.getTimeSpentInHours(entry.timeSpentSeconds)} + [{entry.issue.key}] +

{entry.comment}

+ + )); + } + + render() { + return ( +
+
+ + + + + + + + + {this.showTimeframe(this.props.worklogs.today, "Today")} + + {this.showTimeframe(this.props.worklogs.yesterday, "Yesterday")} + + {this.showTimeframe(this.props.worklogs.remainingWeek, "This week")} + + {this.showTimeframe(this.props.worklogs.lastWeek, "Last week")} +
Last entries + this.props.refresh()}> + + +
+
+ +
+ +
+
+ ); + } +} diff --git a/app/components/dashboard/Dashboard.scss b/app/components/dashboard/Dashboard.scss new file mode 100644 index 0000000..291c55e --- /dev/null +++ b/app/components/dashboard/Dashboard.scss @@ -0,0 +1,93 @@ +.dashboard { + display: flex; + flex-direction: column; + height: 100%; + + &__content { + flex: 1; + + overflow: scroll; + + & > table { + width: 100%; + border-collapse: collapse; + + tr:hover > td { + background-color: #eee; + + cursor: pointer; + } + + th { + background-color: #161f28; + color: #ccc; + text-align: left; + font-weight: 400; + + padding: 10px 5px 5px; + } + + td { + background-color: #fff; + color: #333; + + padding: 5px; + border-bottom: 1px solid #ddd; + } + + tr.dashboard__content__sep td { + background-color: #f8f8f8; + font-size: 11px; + text-transform: uppercase; + color: #aaa; + + padding: 10px 6px 4px; + } + + tr.is-active td { + background-color: #243342; + color: #D7DADB; + } + } + + &__truncated { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + + margin: 0; + width: 380px; + } + + &__refresh { + color: #ccc; + + &:hover { + color: #fff; + + transition: color .3s; + } + + &:active > i { + transition: all .1s; + transform: rotate(90deg); + } + } + + &__sep { + & > td:last-of-type { + text-align: right; + + & > a { + color: #333; + + margin-left: 10px; + } + } + } + } + + &__stats-recap { + flex: 0 0 100px; + } +} diff --git a/app/components/issues/Issues.js b/app/components/issues/Issues.js new file mode 100644 index 0000000..f8d40b5 --- /dev/null +++ b/app/components/issues/Issues.js @@ -0,0 +1,119 @@ +import React, {Component} from 'react'; +import {shell} from 'electron'; +import {Link} from 'react-router'; + +import * as StorageService from '../../services/StorageService'; +import * as IssueService from '../../services/IssueService'; + +const ISSUE_BASE_URL = "https://arcbees.atlassian.net/browse"; + +export default class Issues extends Component { + constructor(props) { + super(props); + + this.state = { + issues: [], + filteredIssues: [], + sortedIssues: [], + searchQuery: "", + viewType: StorageService.getViewType(), + issuesStatusSortingOrder: [3,10601,10400,10600,10100] + }; + } + + componentDidMount() { + this.fetchIssues(); + } + + fetchIssues() { + let projectId = this.state.viewType === 'timer' ? StorageService.getActiveProjectId() : StorageService.getWorklogProjectId(); + IssueService.getIssues(projectId).then(issues => { + this.setState({issues: issues.issues, filteredIssues: issues.issues}) + this.sortIssuesByStatus(this.state.filteredIssues); + }); + } + + sortIssuesByStatus(issues) { + let sortedIssues = []; + this.state.issuesStatusSortingOrder.map((issueStatus, key) => { + sortedIssues[key] = issues.filter(issue => { + return issue && issue.fields.status.id == issueStatus; + }); + }); + sortedIssues[sortedIssues.length] = issues.filter(issue => { + return issue && !_.includes(this.state.issuesStatusSortingOrder, parseInt(issue.fields.status.id)); + }); + this.setState({sortedIssues: sortedIssues}); + } + + issueSelected(issueId, issueKey) { + if (this.state.viewType === 'timer') { + StorageService.setActiveIssueId(issueId); + StorageService.setActiveIssueKey(issueKey); + } else { + StorageService.setWorklogIssueId(issueId); + StorageService.setWorklogIssueKey(issueKey); + } + window.eventEmitter.emitEvent('issueChange'); + } + + searchChange(event) { + const searchQuery = event.target.value; + this.setState({ + searchQuery: searchQuery, + filteredIssues: this.filterIssues(searchQuery.toLowerCase()) + }); + } + + filterIssues(searchQuery) { + const filteredIssues = this.state.issues.filter(issue => { + return issue && + (issue.key.toLowerCase().indexOf(searchQuery) >= 0 || issue.fields.summary.toLowerCase().indexOf(searchQuery) >= 0); + }); + this.sortIssuesByStatus(filteredIssues); + return filteredIssues; + } + + showIssues() { + return this.state.sortedIssues.map((issues, key) => { + if (issues.length > 0) { + return ( +
+

{issues[0].fields.status.name}

+
    + {this.renderIssues(issues)} +
+
+ ) + } + }); + } + + renderIssues(issues) { + return issues.map((issue) => ( +
  • + this.issueSelected(issue.id, issue.key)} className="page__item__link page__item__link--with-icon"> + {issue.key}{issue.fields.summary} + + shell.openExternal(ISSUE_BASE_URL + "/" + issue.key)}> + + +
  • + )); + } + + render() { + return ( +
    +
    +

    Tasks

    + + + + +
    + {this.showIssues()} +
    + ); + } +} diff --git a/app/components/login/Login.js b/app/components/login/Login.js new file mode 100644 index 0000000..9c09f37 --- /dev/null +++ b/app/components/login/Login.js @@ -0,0 +1,73 @@ +import React, {Component} from 'react'; + +import * as StorageService from '../../services/StorageService'; + +import './Login.scss'; + +export default class Login extends Component { + constructor(props) { + super(props); + this.state = { + instanceURL: StorageService.getInstanceURL() + } + } + + submitForm(event) { + event.preventDefault(); + + const instanceURL = this.state.instanceURL.trim(); + const username = this.state.username.trim(); + const password = this.state.password.trim(); + if (!username || !password || !instanceURL) { + return; + } + + this.props.onFormSubmit({instanceURL, username, password}); + } + + handleInstanceUrlChange(event) { + this.setState({instanceURL: event.target.value}); + } + + handleUsernameChange(event) { + this.setState({username: event.target.value}); + } + + handlePasswordChange(event) { + this.setState({password: event.target.value}); + } + + render() { + return ( +
    + Tempora + {/*
    + +
    + +

    or

    + */} +
    this.submitForm(e)}> +

    + this.handleInstanceUrlChange(e)} /> +

    +

    + this.handleUsernameChange(e)} /> +

    +

    + this.handlePasswordChange(e)} /> +

    +

    + +

    +
    +
    + ); + } +} diff --git a/app/components/login/Login.scss b/app/components/login/Login.scss new file mode 100644 index 0000000..c915945 --- /dev/null +++ b/app/components/login/Login.scss @@ -0,0 +1,69 @@ +@import "../../assets/scss/settings/colors"; + +.login { + background-color: #fff; + + display: flex; + flex-direction: column; + width: 100%; + height: 100vh; + justify-content: center; + align-items: center; + + &__logo { + width: 170px; + height: 146px; + margin-bottom: 20px; + } + + &__google { + border: 1px solid #ccc; + background-color: #f5f5f5; + font-size: 16px; + + width: 300px; + padding: 10px; + border-radius: 3px; + } + + &__sep { + margin: 20px 0 5px; + } + + &__input { + font-size: 16px; + + width: 300px; + padding: 10px; + border: 1px solid #ddd; + + &:focus { + border: 1px solid $c-primary--dark; + + outline: 0; + } + } + + &__btn { + background-color: $c-primary; + color: #fff; + font-size: 16px; + text-align: center; + + width: 150px; + border: 0; + border-radius: 3px; + padding: 12px; + margin-left: 75px; + + &:focus { + outline: 0; + } + + &:hover { + background-color: $c-primary--dark; + + cursor: pointer; + } + } +} diff --git a/app/components/projects/Projects.js b/app/components/projects/Projects.js new file mode 100644 index 0000000..bd30ef8 --- /dev/null +++ b/app/components/projects/Projects.js @@ -0,0 +1,104 @@ +import React, {Component} from 'react'; +import {shell} from 'electron'; +import {Link} from 'react-router'; + +import * as StorageService from '../../services/StorageService'; +import * as ProjectService from '../../services/ProjectService'; + +const PROJECT_BASE_URL = "https://arcbees.atlassian.net/projects"; + +export default class Projects extends Component { + constructor(props) { + super(props); + + this.state = { + projects: [], + filteredProjects: [], + latestProjects: [], + searchQuery: "", + viewType: StorageService.getViewType() + }; + } + + componentDidMount() { + this.fetchProjects(); + this.fetchLatestProjects(); + } + + fetchProjects() { + ProjectService.getProjects().then(projects => this.setState({projects: projects, filteredProjects: projects})); + } + + fetchLatestProjects() { + ProjectService.getLatestProjects().then(latestProjects => this.setState({latestProjects: latestProjects, filteredLatestProjects: latestProjects})); + } + + showLatestProjects() { + return this.renderProjects(this.state.filteredLatestProjects); + } + + showProjects() { + return this.renderProjects(this.state.filteredProjects); + } + + projectSelected(projectId) { + if (this.state.viewType === 'timer') { + StorageService.setActiveProjectId(projectId); + } else { + StorageService.setWorklogProjectId(projectId); + } + window.eventEmitter.emitEvent('projectChange'); + } + + searchChange(event) { + const searchQuery = event.target.value; + this.setState({ + searchQuery: searchQuery, + filteredProjects: this.filterProjects(this.state.projects, searchQuery), + filteredLatestProjects: this.filterProjects(this.state.latestProjects, searchQuery) + }); + } + + filterProjects(projects, searchQuery) { + return projects.filter(key => { + return key && key.name.toLowerCase().indexOf(searchQuery) >= 0; + }); + } + + renderProjects(projects) { + if (projects) { + return projects.map((project) => ( +
  • + this.projectSelected(project.id)} className="page__item__link page__item__link--with-icon">{project.name} + shell.openExternal(PROJECT_BASE_URL + "/" + project.key)}> + + +
  • + )); + } else { + return

    No projects found

    ; + } + } + + render() { + return ( +
    +
    +

    Projects

    + + + + +
    +

    Most recents

    +
      + {this.showLatestProjects()} +
    +

    All projects

    +
      + {this.showProjects()} +
    +
    + ); + } +} diff --git a/app/components/settings/Settings.js b/app/components/settings/Settings.js new file mode 100644 index 0000000..cd08667 --- /dev/null +++ b/app/components/settings/Settings.js @@ -0,0 +1,100 @@ +import React, {Component} from 'react'; +import {Link} from 'react-router'; +import Select from 'react-select'; + +import * as StorageService from '../../services/StorageService'; + +import './Settings.scss'; + +export default class Settings extends Component { + static contextTypes = { + settings: React.PropTypes.object, + user: React.PropTypes.object + }; + + constructor(props) { + super(props); + this.state = { + disabled: false, + clearable: false + }; + } + + updateStateValue(key, newValue) { + window.eventEmitter.emitEvent('settingsHasChanged', [key, newValue]); + } + + logout() { + StorageService.logout(); + } + + render() { + const Select = require('react-select'); + const timeRoundOptions = [ + { value: 0, label: 'Never' }, + { value: 5, label: 'Nearest 5mn' }, + { value: 10, label: 'Nearest 10mn' }, + { value: 15, label: 'Nearest 15mn' }, + { value: 20, label: 'Nearest 20mn' }, + { value: 30, label: 'Nearest 30mn' }, + { value: 60, label: 'Nearest 1h' } + ]; + const inactivityOptions = [ + { value: 0, label: 'Never' }, + { value: 5, label: 'After 5mn' }, + { value: 15, label: 'After 15mn' }, + { value: 60, label: 'After 1h' } + ]; + + return ( +
    +
    +

    Settings

    + + + +
    +

    Profil

    +
    User
    +
    + +

    + {this.context.user.displayName}
    + {this.context.user.emailAddress}
    +

    +

    + this.logout()}>Logout +

    +
    +
    +

    Options

    +
    +
    + +
    + this.updateStateValue('inactivity', newValue)} + value={this.context.settings.inactivity} + clearable={false} + /> +
    +
    */} +
    +
    + ); + } +} diff --git a/app/components/settings/Settings.scss b/app/components/settings/Settings.scss new file mode 100644 index 0000000..3598923 --- /dev/null +++ b/app/components/settings/Settings.scss @@ -0,0 +1,22 @@ +@import "../../assets/scss/settings/colors"; + +.settings { + &__label { + padding: 8px 0 8px 8px; + border-left: 2px solid $c-primary; + } + + &__avatar { + float: right; + margin-top: 12px; + } + + &__link { + color: $c-primary; + text-decoration: none; + + &:hover { + text-decoration: underline; + } + } +} diff --git a/app/components/sidebar/Sidebar.js b/app/components/sidebar/Sidebar.js new file mode 100644 index 0000000..2dff027 --- /dev/null +++ b/app/components/sidebar/Sidebar.js @@ -0,0 +1,19 @@ +import React, {Component} from 'react'; +import {Link} from 'react-router'; +import Timer from '../timer/Timer'; +import './Sidebar.scss'; + +export default class Sidebar extends Component { + render() { + return ( +
    +
    + +
    +
    + +
    +
    + ); + } +} diff --git a/app/components/sidebar/Sidebar.scss b/app/components/sidebar/Sidebar.scss new file mode 100644 index 0000000..58131b1 --- /dev/null +++ b/app/components/sidebar/Sidebar.scss @@ -0,0 +1,20 @@ +.sidebar { + display: flex; + flex-direction: column; + height: 100%; + + &__timer { + flex: 1; + overflow: scroll; + } + + &__options { + background-color: #161f28; + + padding: 10px 20px; + + & a { + color: #aaa; + } + } +} diff --git a/app/components/stats-recap/StatsRecap.js b/app/components/stats-recap/StatsRecap.js new file mode 100644 index 0000000..e758ba3 --- /dev/null +++ b/app/components/stats-recap/StatsRecap.js @@ -0,0 +1,29 @@ +import React, {Component} from 'react'; +import {Link} from 'react-router'; +import * as WorklogService from '../../services/WorklogService'; +import './StatsRecap.scss'; + +export default class StatsRecap extends Component { + constructor(props) { + super(props); + } + + render() { + return ( +
    +
    +
    {this.props.stats.lastWeekTime}
    +
    Last week
    +
    +
    +
    {this.props.stats.weekTime}
    +
    This week
    +
    +
    +
    {this.props.stats.todayTime}
    +
    Today
    +
    +
    + ); + } +} diff --git a/app/components/stats-recap/StatsRecap.scss b/app/components/stats-recap/StatsRecap.scss new file mode 100644 index 0000000..7f6a769 --- /dev/null +++ b/app/components/stats-recap/StatsRecap.scss @@ -0,0 +1,26 @@ +@import "../../assets/scss/settings/colors"; + +.stats-recap { + background-color: $c-primary; + color: #fff; + + height: 100%; + display: flex; + align-items: center; + justify-content: space-around; + + &__stat { + flex: 0 0 33.3%; + padding: 0 20px; + + &__data { + font-size: 44px; + font-weight: 700; + } + + &__label { + text-transform: uppercase; + font-size: 12px; + } + } +} diff --git a/app/components/timer/Timer.js b/app/components/timer/Timer.js new file mode 100644 index 0000000..36db0c2 --- /dev/null +++ b/app/components/timer/Timer.js @@ -0,0 +1,407 @@ +import moment from 'moment'; +import React, { Component } from 'react'; +import ReactTimeout from 'react-timeout'; +import DatePicker from 'react-datepicker'; +import TimePicker from 'rc-time-picker'; +import { Link } from 'react-router'; + +import * as StorageService from '../../services/StorageService'; +import * as WorklogService from '../../services/WorklogService'; +import * as ProjectService from '../../services/ProjectService'; +import * as IssueService from '../../services/IssueService'; +import * as DateUtils from '../../utils/DateUtils'; + +import './Timer.scss'; + +const STATE = { + time: "0:00", + timeSeconds: "00", + projectId: StorageService.getActiveProjectId(), + projectName: StorageService.getActiveProjectName(), + issueId: StorageService.getActiveIssueId(), + issueKey: StorageService.getActiveIssueKey(), + date: StorageService.getActiveDate() ? StorageService.getActiveDate() : moment(), + comment: StorageService.getActiveComment() ? StorageService.getActiveComment() : "", + selectedWorklogId: null, + viewType: StorageService.getViewType() ? StorageService.getViewType() : "timer" +}; + +export default class Timer extends Component { + static contextTypes = { + settings: React.PropTypes.object, + user: React.PropTypes.object + }; + + constructor(props) { + super(props); + + this.state = STATE; + this.time = 0; + this.showTimer(); + + window.eventEmitter.addListener('selectWorklog', this.setSelectedWorklog.bind(this)); + window.eventEmitter.addListener('projectChange', this.fetchProject.bind(this)); + window.eventEmitter.addListener('issueChange', this.fetchIssue.bind(this)); + } + + // Timer states + + startTimer() { + StorageService.setDateStartMoment(moment().toISOString()); + StorageService.startTimer(); + this.tick(); + } + + showTimer() { + window.eventEmitter.emitEvent('unselectActiveWorklog'); + + this.setState({ + time: "0:00", + timeSeconds: "00", + projectName: StorageService.getActiveProjectName(), + projectId: StorageService.getActiveProjectId(), + issueId: StorageService.getActiveIssueId(), + issueKey: StorageService.getActiveIssueKey(), + comment: StorageService.getActiveComment() ? StorageService.getActiveComment() : "", + date: StorageService.getActiveDate() ? StorageService.getActiveDate() : moment(), + selectedWorklogId: null, + viewType: 'timer' + }); + StorageService.setViewType('timer'); + + if (StorageService.isTimerStarted() === 'true') { + if (StorageService.isTimerPaused() === 'false') { + this.tick(); + } else { + const startTime = StorageService.getDateStartMoment(); + const endTime = StorageService.getDatePauseMoment(); + this.time = this.getDurationBetweenDates(moment(endTime), moment(startTime)); + this.setState({ + time: this.getTimeFromMilliseconds(), + timeSeconds: this.getSecondsFromDuration() + }); + } + } + } + + pauseTimer() { + StorageService.setDatePauseMoment(moment().toISOString()); + StorageService.pauseTimer(); + } + + unpauseTimer() { + const startedAt = moment(StorageService.getDateStartMoment()); + const pausedAt = moment(StorageService.getDatePauseMoment()); + const timeElapsed = pausedAt.diff(startedAt); + + StorageService.setDateStartMoment(moment().subtract(moment.duration(timeElapsed)).toISOString()); + StorageService.unpauseTimer(); + this.tick(); + } + + stopTimer() { + if (!StorageService.getActiveProjectId()) { + window.eventEmitter.emitEvent('requestModal', ["Oups!", "You need to select a project."]); + } else if (!StorageService.getActiveIssueKey()) { + window.eventEmitter.emitEvent('requestModal', ["Oups!", "You need to select a task."]); + } else { + StorageService.stopTimer(); + StorageService.setTimeSpentInSeconds(Math.round(moment.duration(this.time).asSeconds())); + + let worklog = this.buildWorklog(); + + WorklogService.createWorklog(worklog) + .then(() => { + window.eventEmitter.emitEvent('refreshWorklogsAndTime'); + this.setState({ + time: '0:00', + timeSeconds: '00' + }); + this.clearData(); + }) + .catch(error => window.eventEmitter.emitEvent('requestModal', ["Oups!", error])); + } + } + + // Worklogs + + setSelectedWorklog(worklog) { + this.setState({ + selectedWorklogId: worklog.id, + projectId: worklog.issue.projectId, + issueId: worklog.issue.id, + issueKey: worklog.issue.key, + viewType: 'worklog' + }); + StorageService.setWorklogProjectId(worklog.issue.projectId); + StorageService.setViewType('worklog'); + + // TODO: Find a lighter way to get the project name than fetching the entire project + ProjectService.getProject(worklog.issue.projectId) + .then(project => { + this.setState({ + projectName: project.name, + duration: worklog.timeSpentSeconds, + time: WorklogService.getTimeSpentInHours(worklog.timeSpentSeconds), + timeSeconds: "00", + issue: worklog.issue.key, + date: worklog.dateStarted, + comment: worklog.comment + }); + StorageService.setWorklogProjectName(project.name); + }); + } + + saveSelectedWorklog() { + let worklog = this.buildWorklogForUpdate(); + + WorklogService.updateWorklog(this.state.selectedWorklogId, worklog) + .then(() => { + window.eventEmitter.emitEvent('refreshWorklogsAndTime'); + this.showTimer(); + }) + .catch(error => window.eventEmitter.emitEvent('requestModal', ["Oups!", error])); + } + + deleteSelectedWorklog() { + if (confirm("Are you sure you want to delete this entry?")) { + WorklogService.deleteWorklog(this.state.selectedWorklogId) + .then(() => { + window.eventEmitter.emitEvent('refreshWorklogsAndTime'); + this.showTimer(); + }) + .catch(error => window.eventEmitter.emitEvent('requestModal', ["Oups!", error])); + } + } + + // Functions and helpers + + tick() { + let startTime = StorageService.getDateStartMoment(); + + this.time = this.getDurationBetweenDates(moment(), moment(startTime)); + this.setState({ + time: this.getTimeFromMilliseconds(), + timeSeconds: this.getSecondsFromDuration() + }); + + if (StorageService.isTimerStarted() === 'true' && StorageService.isTimerPaused() === 'false') { + // The tick will be called multiple times per second but it will result + // in a quicker response when clicking on the play/stop button + let timeout = setTimeout(() => this.tick(), 500); + } + } + + buildWorklog() { + return { + author: { + name: this.context.user.name + }, + issue: { + projectId: StorageService.getActiveProjectId(), + key: StorageService.getActiveIssueKey() + }, + timeSpentSeconds: this.getRoundedTime(), + dateStarted: StorageService.getDateStartMoment(), + comment: this.state.comment + }; + } + + buildWorklogForUpdate() { + return { + timeSpentSeconds: this.state.duration > 0 ? this.state.duration : 60, + dateStarted: moment(this.state.date).toISOString(), + comment: this.state.comment, + issue: { + projectId: this.state.projectId, + key: this.state.issueKey + } + }; + } + + getTimeFromMilliseconds() { + return moment.duration(this.time, 'milliseconds').format(DateUtils.DateFormat.TIME, { trim: false }); + } + + getDurationBetweenDates(now, then) { + return now.diff(then); + } + + getSecondsFromDuration() { + let seconds = moment.duration(this.time, 'milliseconds').seconds() + return seconds <= 9 ? "0" + seconds : seconds; + } + + getRoundedTime() { + const round = this.context.settings.timeRound * 60; + const seconds = StorageService.getTimeSpentInSeconds(); + if(round > 0) { + if(seconds < round) { + return round; + } + const rounded = (seconds % round) >= round / 2 ? parseInt(seconds / round) * round + round : parseInt(seconds / round) * round; + StorageService.setTimeSpentInSeconds(rounded); + return rounded; + } else { + return seconds; + } + } + + fetchProject() { + if (this.state.viewType === 'timer') { + StorageService.setActiveIssueId(""); + StorageService.setActiveIssueKey(""); + } else { + StorageService.setWorklogIssueId(""); + StorageService.setWorklogIssueKey(""); + } + + ProjectService.getProject(this.state.viewType === 'timer' ? StorageService.getActiveProjectId() : StorageService.getWorklogProjectId()) + .then(project => { + this.setState({ + projectId: project.id, + projectName: project.name, + time: "0:00", + timeSeconds: "00", + issueId: "", + issueKey: "-", + date: moment(), + comment: "" + }); + if (this.state.viewType === 'timer') { + StorageService.setActiveProjectId(project.id); + StorageService.setActiveProjectName(project.name); + if (StorageService.isTimerStarted() === 'true' && StorageService.isTimerPaused() === 'false') { + this.tick(); + } + } else { + StorageService.setWorklogProjectId(project.id); + StorageService.setWorklogProjectName(project.name); + } + }); + } + + fetchIssue() { + IssueService.getIssue(this.state.viewType === 'timer' ? StorageService.getActiveIssueId() : StorageService.getWorklogIssueId()) + .then(issue => this.setState({ + issueId: issue.id, + issueKey: issue.key + })); + } + + clearData() { + StorageService.clearActiveTimer(); + StorageService.stopTimer(); + this.setState(STATE); + } + + clearDataConfirm() { + if (confirm("Are you sure you want to delete this?")) { + this.clearData(); + } + } + + // Handling components change + + timeChange(time) { + this.setState({ duration: ((time.minutes() * 60) + (time.hours() * 3600)) }); + } + + dateChange(date) { + this.setState({ + date: date + }); + + if (this.state.viewType === 'timer') { + StorageService.setActiveDate(moment(date).toISOString()); + } + } + + commentChange(event) { + this.setState({ + comment: event.target.value + }); + + if (this.state.viewType === 'timer') { + StorageService.setActiveComment(this.state.comment); + } + } + + // Template rendering + + renderSeconds() { + if (this.state.timeSeconds) { + return (.{this.state.timeSeconds}); + } + } + + renderTime() { + if (this.state.viewType === 'timer') { + return ({this.state.time}{this.renderSeconds()}); + } else { + return ( + + ) + } + } + + renderButtons() { + let buttons = []; + if (this.state.viewType === 'timer') { + if (StorageService.isTimerStarted() === 'true') { + if (StorageService.isTimerPaused() === 'true') { + buttons.push(); + } else { + buttons.push(); + } + buttons.push(); + buttons.push(); + } else { + buttons.push(); + } + } else if (this.state.viewType === 'worklog') { + buttons.push(); + buttons.push(); + buttons.push(); + } + return ( + + {buttons} + + ); + } + + render() { + return ( +
    +
    + {this.renderTime()} + {this.renderButtons()} +
    +
    + +

    Project

    +

    {this.state.projectName}

    + + +

    Task

    +

    {this.state.issueKey}

    + +
    +

    Date

    + +
    +
    +

    Comment

    +