├── .eslintignore ├── .eslintrc ├── .eslintrc-meteor ├── .gitignore ├── .meteor ├── .finished-upgraders ├── .gitignore ├── .id ├── packages ├── platforms ├── release └── versions ├── README.md ├── client ├── index.html ├── main.js └── main.less ├── imports ├── api │ ├── lists │ │ ├── lists.js │ │ ├── methods.js │ │ └── publications.js │ └── todos │ │ ├── incompleteCountDenormalizer.js │ │ ├── methods.js │ │ ├── publications.js │ │ └── todos.js ├── startup │ ├── client │ │ └── routes.jsx │ └── server │ │ ├── fixtures.js │ │ ├── register-api.js │ │ └── security.js └── ui │ ├── components │ ├── ConnectionNotification.jsx │ ├── ConnectionNotification.less │ ├── ListHeader.jsx │ ├── ListHeader.less │ ├── ListList.jsx │ ├── ListList.less │ ├── Loading.jsx │ ├── Loading.less │ ├── Message.jsx │ ├── Message.less │ ├── MobileMenu.jsx │ ├── TodoItem.jsx │ ├── TodoItem.less │ ├── UserMenu.jsx │ └── UserMenu.less │ ├── containers │ ├── AppContainer.jsx │ └── ListContainer.jsx │ ├── helpers │ ├── create-container-with-komposer.js │ └── create-container.jsx │ ├── layouts │ ├── App.jsx │ └── App.less │ ├── pages │ ├── AuthPage.jsx │ ├── AuthPage.less │ ├── AuthPageJoin.jsx │ ├── AuthPageSignIn.jsx │ ├── ListPage.jsx │ ├── ListPage.less │ ├── NotFoundPage.jsx │ └── NotFoundPage.less │ └── stylesheets │ ├── base.less │ ├── button.less │ ├── fade-transition.less │ ├── form.less │ ├── icon.less │ ├── link.less │ ├── nav.less │ ├── reset.less │ ├── util │ ├── fontface.less │ ├── helpers.less │ ├── text.less │ ├── typography.less │ └── variables.less │ └── utils.less ├── package.json ├── packages └── factory │ ├── README.md │ ├── dataset.js │ ├── factory-api.js │ ├── factory-tests.js │ ├── factory.js │ └── package.js ├── public ├── apple-touch-icon-precomposed.png ├── favicon.png ├── font │ ├── OpenSans-Light-webfont.eot │ ├── OpenSans-Light-webfont.svg │ ├── OpenSans-Light-webfont.ttf │ ├── OpenSans-Light-webfont.woff │ ├── OpenSans-Regular-webfont.eot │ ├── OpenSans-Regular-webfont.svg │ ├── OpenSans-Regular-webfont.ttf │ └── OpenSans-Regular-webfont.woff ├── icon │ ├── todos.eot │ ├── todos.svg │ ├── todos.ttf │ └── todos.woff └── logo-todos.svg └── server └── main.js /.eslintignore: -------------------------------------------------------------------------------- 1 | packages 2 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | /* Galaxy's Javascript linting configuration (Meteor) 2 | * 3 | * Documentation on rules can be found at: 4 | * http://eslint.org/docs/rules/ <- Optionally append the rulename 5 | * 6 | */ 7 | 8 | { 9 | "extends": [".eslintrc-meteor"], 10 | "plugins": [ 11 | "react" 12 | ], 13 | "rules": { 14 | "no-alert": 0, 15 | // allow pure function components to be capitalized 16 | "new-cap": 0, 17 | // common for pure function components to be wrapped in parens 18 | "no-extra-parens": 0, 19 | // common for conditional render functions 20 | "no-else-return": 0, 21 | /** 22 | * React specific 23 | */ 24 | "jsx-quotes": [1, "prefer-double"], 25 | "react/display-name": [1, { "acceptTranspilerName": true }], 26 | "react/forbid-prop-types": 0, 27 | "react/jsx-boolean-value": 1, 28 | "react/jsx-closing-bracket-location": 0, 29 | "react/jsx-curly-spacing": 1, 30 | "react/jsx-equals-spacing": 1, 31 | "react/jsx-handler-names": 1, 32 | "react/jsx-indent-props": 0, 33 | "react/jsx-indent": 0, 34 | "react/jsx-key": 1, 35 | "react/jsx-max-props-per-line": 0, 36 | "react/jsx-no-bind": 0, 37 | "react/jsx-no-duplicate-props": 1, 38 | "react/jsx-no-literals": 0, 39 | "react/jsx-no-undef": 1, 40 | "react/jsx-pascal-case": 1, 41 | "react/jsx-sort-prop-types": 0, 42 | "react/jsx-sort-props": 0, 43 | "react/jsx-uses-react": 1, 44 | "react/jsx-uses-vars": 1, 45 | "react/no-danger": 1, 46 | "react/no-deprecated": 1, 47 | "react/no-did-mount-set-state": 1, 48 | "react/no-did-update-set-state": 1, 49 | "react/no-direct-mutation-state": 1, 50 | "react/no-is-mounted": 1, 51 | "react/no-multi-comp": 0, 52 | "react/no-set-state": 0, 53 | "react/no-string-refs": 0, 54 | "react/no-unknown-property": 1, 55 | "react/prefer-es6-class": 1, 56 | "react/prop-types": 0, 57 | "react/react-in-jsx-scope": 1, 58 | "react/require-extension": 1, 59 | "react/self-closing-comp": 1, 60 | "react/sort-comp": 0, 61 | "react/wrap-multilines": 1 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /.eslintrc-meteor: -------------------------------------------------------------------------------- 1 | /* Meteor's ES6 Javascript linting configuration 2 | * 3 | * Documentation on rules can be found at: 4 | * http://eslint.org/docs/rules/ <- Optionally append the rulename 5 | * 6 | */ 7 | { 8 | "parser": "babel-eslint", 9 | "env": { 10 | "browser": true, 11 | "meteor": true, 12 | "node": true, 13 | }, 14 | "ecmaFeatures": { 15 | "arrowFunctions": true, 16 | "blockBindings": true, 17 | "classes": true, 18 | "defaultParams": true, 19 | "destructuring": true, 20 | "forOf": true, 21 | "generators": false, 22 | "modules": true, 23 | "objectLiteralComputedProperties": true, 24 | "objectLiteralDuplicateProperties": false, 25 | "objectLiteralShorthandMethods": true, 26 | "objectLiteralShorthandProperties": true, 27 | "restParams": true, 28 | "spread": true, 29 | "superInFunctions": true, 30 | "templateStrings": true, 31 | "jsx": true, 32 | }, 33 | "rules": { 34 | 35 | /** 36 | * Meteor Specific 37 | */ 38 | // babel inserts "use strict"; for us 39 | // http://eslint.org/docs/rules/strict 40 | "strict": [2, "never"], 41 | 42 | // allows certain non-constructor functions to start with a capital letter 43 | "new-cap": [2, { // http://eslint.org/docs/rules/new-cap 44 | "capIsNewExceptions": [ 45 | "Match", "Any", "Object", "ObjectIncluding", "OneOf", "Optional", "Where" 46 | ] 47 | }], 48 | 49 | /** 50 | * ES6 Specific 51 | */ 52 | "arrow-parens": 0, // http://eslint.org/docs/rules/arrow-parens 53 | "arrow-spacing": 2, // http://eslint.org/docs/rules/arrow-spacing 54 | "constructor-super": 2, // http://eslint.org/docs/rules/constructor-super 55 | "generator-star-spacing": 2, // http://eslint.org/docs/rules/generator-star-spacing 56 | "no-class-assign": 2, // http://eslint.org/docs/rules/no-class-assign 57 | "no-const-assign": 2, // http://eslint.org/docs/rules/no-const-assign 58 | "no-dupe-class-members": 2, // http://eslint.org/docs/rules/no-dupe-class-members 59 | "no-this-before-super": 2, // http://eslint.org/docs/rules/no-this-before-super 60 | "no-var": 2, // http://eslint.org/docs/rules/no-var 61 | "object-shorthand": 0, // http://eslint.org/docs/rules/object-shorthand 62 | "prefer-arrow-callback": 1, // http://eslint.org/docs/rules/prefer-arrow-callback 63 | "prefer-const": 2, // http://eslint.org/docs/rules/prefer-const 64 | "prefer-spread": 2, // http://eslint.org/docs/rules/prefer-spread 65 | "prefer-template": 2, // http://eslint.org/docs/rules/prefer-template 66 | "require-yield": 2, // http://eslint.org/docs/rules/require-yield 67 | 68 | /** 69 | * Variables 70 | */ 71 | "no-shadow": 2, // http://eslint.org/docs/rules/no-shadow 72 | "no-shadow-restricted-names": 2, // http://eslint.org/docs/rules/no-shadow-restricted-names 73 | "no-unused-vars": [2, { // http://eslint.org/docs/rules/no-unused-vars 74 | "vars": "local", 75 | "args": "after-used" 76 | }], 77 | "no-use-before-define": [2, "nofunc"], // http://eslint.org/docs/rules/no-use-before-define 78 | 79 | /** 80 | * Possible errors 81 | */ 82 | "block-scoped-var": 0, // http://eslint.org/docs/rules/block-scoped-var 83 | "no-alert": 1, // http://eslint.org/docs/rules/no-alert 84 | "no-cond-assign": [1, "always"], // http://eslint.org/docs/rules/no-cond-assign 85 | "no-console": 1, // http://eslint.org/docs/rules/no-console 86 | "no-constant-condition": 1, // http://eslint.org/docs/rules/no-constant-condition 87 | "no-debugger": 1, // http://eslint.org/docs/rules/no-debugger 88 | "no-dupe-keys": 2, // http://eslint.org/docs/rules/no-dupe-keys 89 | "no-duplicate-case": 2, // http://eslint.org/docs/rules/no-duplicate-case 90 | "no-empty": 2, // http://eslint.org/docs/rules/no-empty 91 | "no-empty-label": 2, // http://eslint.org/docs/rules/no-empty-label 92 | "no-ex-assign": 2, // http://eslint.org/docs/rules/no-ex-assign 93 | "no-extra-boolean-cast": 0, // http://eslint.org/docs/rules/no-extra-boolean-cast 94 | "no-extra-semi": 2, // http://eslint.org/docs/rules/no-extra-semi 95 | "no-func-assign": 2, // http://eslint.org/docs/rules/no-func-assign 96 | "no-inner-declarations": 2, // http://eslint.org/docs/rules/no-inner-declarations 97 | "no-invalid-regexp": 2, // http://eslint.org/docs/rules/no-invalid-regexp 98 | "no-irregular-whitespace": 2, // http://eslint.org/docs/rules/no-irregular-whitespace 99 | "no-obj-calls": 2, // http://eslint.org/docs/rules/no-obj-calls 100 | "no-sparse-arrays": 2, // http://eslint.org/docs/rules/no-sparse-arrays 101 | "no-undef": 2, // http://eslint.org/docs/rules/no-undef 102 | "no-unreachable": 2, // http://eslint.org/docs/rules/no-unreachable 103 | "quote-props": [0, "as-needed"], // http://eslint.org/docs/rules/quote-props 104 | "use-isnan": 2, // http://eslint.org/docs/rules/use-isnan 105 | 106 | /** 107 | * Best practices 108 | */ 109 | "consistent-return": 2, // http://eslint.org/docs/rules/consistent-return 110 | "curly": [2, "multi-line"], // http://eslint.org/docs/rules/curly 111 | "default-case": 2, // http://eslint.org/docs/rules/default-case 112 | "dot-notation": [2, { // http://eslint.org/docs/rules/dot-notation 113 | "allowKeywords": true 114 | }], 115 | "eqeqeq": 2, // http://eslint.org/docs/rules/eqeqeq 116 | "guard-for-in": 2, // http://eslint.org/docs/rules/guard-for-in 117 | "max-len": [1, 100, 2, { // http://eslint.org/docs/rules/max-len 118 | "ignoreUrls": true, "ignorePattern": "['\"]" 119 | }], 120 | "no-caller": 2, // http://eslint.org/docs/rules/no-caller 121 | "no-else-return": 2, // http://eslint.org/docs/rules/no-else-return 122 | "no-eq-null": 2, // http://eslint.org/docs/rules/no-eq-null 123 | "no-eval": 2, // http://eslint.org/docs/rules/no-eval 124 | "no-extend-native": 2, // http://eslint.org/docs/rules/no-extend-native 125 | "no-extra-bind": 2, // http://eslint.org/docs/rules/no-extra-bind 126 | "no-fallthrough": 2, // http://eslint.org/docs/rules/no-fallthrough 127 | "no-floating-decimal": 2, // http://eslint.org/docs/rules/no-floating-decimal 128 | "no-implied-eval": 2, // http://eslint.org/docs/rules/no-implied-eval 129 | "no-iterator": 2, // http://eslint.org/docs/rules/no-iterator 130 | "no-label-var": 2, // http://eslint.org/docs/rules/no-label-var 131 | "no-lone-blocks": 2, // http://eslint.org/docs/rules/no-lone-blocks 132 | "no-loop-func": 2, // http://eslint.org/docs/rules/no-loop-func 133 | "no-multi-spaces": 2, // http://eslint.org/docs/rules/no-multi-spaces 134 | "no-multi-str": 2, // http://eslint.org/docs/rules/no-multi-str 135 | "no-native-reassign": 2, // http://eslint.org/docs/rules/no-native-reassign 136 | "no-new": 2, // http://eslint.org/docs/rules/no-new 137 | "no-new-func": 2, // http://eslint.org/docs/rules/no-new-func 138 | "no-new-wrappers": 2, // http://eslint.org/docs/rules/no-new-wrappers 139 | "no-octal": 2, // http://eslint.org/docs/rules/no-octal 140 | "no-octal-escape": 2, // http://eslint.org/docs/rules/no-octal-escape 141 | "no-param-reassign": 0, // http://eslint.org/docs/rules/no-param-reassign 142 | "no-process-exit": 2, // http://eslint.org/docs/rules/no-process-exit 143 | "no-proto": 2, // http://eslint.org/docs/rules/no-proto 144 | "no-redeclare": 2, // http://eslint.org/docs/rules/no-redeclare 145 | "no-return-assign": 2, // http://eslint.org/docs/rules/no-return-assign 146 | "no-script-url": 2, // http://eslint.org/docs/rules/no-script-url 147 | "no-self-compare": 2, // http://eslint.org/docs/rules/no-self-compare 148 | "no-sequences": 2, // http://eslint.org/docs/rules/no-sequences 149 | "no-throw-literal": 2, // http://eslint.org/docs/rules/no-throw-literal 150 | "no-with": 2, // http://eslint.org/docs/rules/no-with 151 | "radix": 2, // http://eslint.org/docs/rules/radix 152 | "vars-on-top": 0, // http://eslint.org/docs/rules/vars-on-top 153 | "wrap-iife": [2, "any"], // http://eslint.org/docs/rules/wrap-iife 154 | "yoda": 2, // http://eslint.org/docs/rules/yoda 155 | 156 | /** 157 | * Style 158 | */ 159 | "indent": [2, 2], // http://eslint.org/docs/rules/indent 160 | "brace-style": [1, // http://eslint.org/docs/rules/brace-style 161 | "1tbs", { 162 | "allowSingleLine": true 163 | }], 164 | "quotes": [ 165 | 1, "single", "avoid-escape" // http://eslint.org/docs/rules/quotes 166 | ], 167 | "camelcase": [2, { // http://eslint.org/docs/rules/camelcase 168 | "properties": "never" 169 | }], 170 | "comma-spacing": [2, { // http://eslint.org/docs/rules/comma-spacing 171 | "before": false, 172 | "after": true 173 | }], 174 | "comma-style": [2, "last"], // http://eslint.org/docs/rules/comma-style 175 | "eol-last": 2, // http://eslint.org/docs/rules/eol-last 176 | "func-names": 0, // http://eslint.org/docs/rules/func-names 177 | "func-style": 0, // http://eslint.org/docs/rules/func-style 178 | "key-spacing": [2, { // http://eslint.org/docs/rules/key-spacing 179 | "beforeColon": false, 180 | "afterColon": true 181 | }], 182 | "new-parens": 1, // http://eslint.org/docs/rules/new-parens 183 | "no-multiple-empty-lines": [2, { // http://eslint.org/docs/rules/no-multiple-empty-lines 184 | "max": 2 185 | }], 186 | "no-nested-ternary": 2, // http://eslint.org/docs/rules/no-nested-ternary 187 | "no-new-object": 2, // http://eslint.org/docs/rules/no-new-object 188 | "no-array-constructor": 2, // http://eslint.org/docs/rules/no-array-constructor 189 | "no-spaced-func": 2, // http://eslint.org/docs/rules/no-spaced-func 190 | "no-trailing-spaces": 2, // http://eslint.org/docs/rules/no-trailing-spaces 191 | "no-extra-parens": 2, // http://eslint.org/docs/rules/no-extra-parens 192 | "no-underscore-dangle": 0, // http://eslint.org/docs/rules/no-underscore-dangle 193 | "one-var": 0, // http://eslint.org/docs/rules/one-var 194 | "padded-blocks": [2, "never"], // http://eslint.org/docs/rules/padded-blocks 195 | "semi": [2, "always"], // http://eslint.org/docs/rules/semi 196 | "semi-spacing": [2, { // http://eslint.org/docs/rules/semi-spacing 197 | "before": false, 198 | "after": true 199 | }], 200 | "space-after-keywords": 2, // http://eslint.org/docs/rules/space-after-keywords 201 | "space-before-blocks": 2, // http://eslint.org/docs/rules/space-before-blocks 202 | "space-before-function-paren": [ // http://eslint.org/docs/rules/space-before-function-paren 203 | 2, "never" 204 | ], 205 | "space-infix-ops": 2, // http://eslint.org/docs/rules/space-infix-ops 206 | "space-return-throw-case": 2, // http://eslint.org/docs/rules/space-return-throw-case 207 | "space-unary-ops": 2, // http://eslint.org/docs/rules/space-unary-ops 208 | "spaced-comment": 2, // http://eslint.org/docs/rules/spaced-comment 209 | } 210 | } 211 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | -------------------------------------------------------------------------------- /.meteor/.finished-upgraders: -------------------------------------------------------------------------------- 1 | # This file contains information which helps Meteor properly upgrade your 2 | # app when you run 'meteor update'. You should check it into version control 3 | # with your project. 4 | 5 | notices-for-0.9.0 6 | notices-for-0.9.1 7 | 0.9.4-platform-file 8 | notices-for-facebook-graph-api-2 9 | 1.2.0-standard-minifiers-package 10 | 1.2.0-meteor-platform-split 11 | 1.2.0-cordova-changes 12 | 1.2.0-breaking-changes 13 | -------------------------------------------------------------------------------- /.meteor/.gitignore: -------------------------------------------------------------------------------- 1 | local 2 | -------------------------------------------------------------------------------- /.meteor/.id: -------------------------------------------------------------------------------- 1 | # This file contains a token that is unique to your project. 2 | # Check it into your repository along with the rest of this directory. 3 | # It can be used for purposes such as: 4 | # - ensuring you don't accidentally deploy one app on top of another 5 | # - providing package authors with aggregated statistics 6 | 7 | 1h6ygz61u4wx7sapvu1c 8 | -------------------------------------------------------------------------------- /.meteor/packages: -------------------------------------------------------------------------------- 1 | # Meteor packages used by this project, one per line. 2 | # Check this file (and the other files in this directory) into your repository. 3 | # 4 | # 'meteor add' and 'meteor remove' will edit this file for you, 5 | # but you can also edit it by hand. 6 | 7 | meteor-base # Packages every Meteor app needs to have 8 | mobile-experience # Packages for a great mobile UX 9 | mongo # The database Meteor supports right now 10 | session # Client-side reactive dictionary for your app 11 | jquery # Helpful client-side library 12 | tracker # Meteor's client-side reactive programming library 13 | 14 | standard-minifiers # JS/CSS minifiers run for production mode 15 | es5-shim # ECMAScript 5 compatibility for older browsers. 16 | ecmascript # Enable ECMAScript2015+ syntax in app code 17 | 18 | aldeed:collection2 19 | aldeed:simple-schema 20 | dburles:collection-helpers 21 | mdg:validated-method 22 | reywood:publish-composite 23 | ddp-rate-limiter 24 | factory 25 | accounts-password 26 | less 27 | check 28 | react-meteor-data 29 | static-html 30 | -------------------------------------------------------------------------------- /.meteor/platforms: -------------------------------------------------------------------------------- 1 | server 2 | browser 3 | -------------------------------------------------------------------------------- /.meteor/release: -------------------------------------------------------------------------------- 1 | METEOR@1.3-modules-beta.8 2 | -------------------------------------------------------------------------------- /.meteor/versions: -------------------------------------------------------------------------------- 1 | accounts-base@1.2.3-modules.8 2 | accounts-password@1.1.5-modules.8 3 | aldeed:collection2@2.8.0 4 | aldeed:collection2-core@1.0.0 5 | aldeed:schema-deny@1.0.1 6 | aldeed:schema-index@1.0.1 7 | aldeed:simple-schema@1.5.3 8 | allow-deny@1.0.1-modules.8 9 | autoupdate@1.2.5-modules.8 10 | babel-compiler@6.4.0-modules.8 11 | babel-runtime@0.1.5-modules.8 12 | base64@1.0.5-modules.8 13 | binary-heap@1.0.5-modules.8 14 | blaze@2.1.4-modules.8 15 | blaze-html-templates@1.0.1 16 | blaze-tools@1.0.5-modules.8 17 | boilerplate-generator@1.0.5-modules.8 18 | caching-compiler@1.0.1-modules.8 19 | caching-html-compiler@1.0.3-modules.8 20 | callback-hook@1.0.5-modules.8 21 | check@1.1.1-modules.8 22 | coffeescript@1.0.12-modules.8 23 | dburles:collection-helpers@1.0.4 24 | ddp@1.2.2 25 | ddp-client@1.2.2-modules.8 26 | ddp-common@1.2.2 27 | ddp-rate-limiter@1.0.1-modules.8 28 | ddp-server@1.2.3-modules.8 29 | deps@1.0.9 30 | dfischer:faker@1.0.8 31 | diff-sequence@1.0.2-modules.8 32 | ecmascript@0.4.0-modules.8 33 | ecmascript-runtime@0.2.7-modules.8 34 | ejson@1.0.8-modules.8 35 | email@1.0.9-modules.8 36 | es5-shim@4.3.2-modules.8 37 | factory@1.0.0 38 | fastclick@1.0.8-modules.8 39 | geojson-utils@1.0.5-modules.8 40 | hot-code-push@1.0.1-modules.8 41 | html-tools@1.0.6-modules.8 42 | htmljs@1.0.6-modules.8 43 | http@1.1.2-modules.8 44 | id-map@1.0.4 45 | jquery@1.11.5-modules.8 46 | jsx@0.2.4 47 | launch-screen@1.0.5-modules.8 48 | less@2.5.2-modules.8 49 | livedata@1.0.15 50 | localstorage@1.0.6-modules.8 51 | logging@1.0.9-modules.8 52 | mdg:validated-method@1.0.1 53 | mdg:validation-error@0.2.0 54 | meteor@1.1.11-modules.8 55 | meteor-base@1.0.1 56 | meteor-env-dev@0.0.1-modules.8 57 | meteor-env-prod@0.0.1-modules.8 58 | minifiers-css@1.1.8-modules.8 59 | minifiers-js@1.1.8-modules.8 60 | minimongo@1.0.11-modules.8 61 | mobile-experience@1.0.1 62 | mobile-status-bar@1.0.6 63 | modules@0.5.0-modules.8 64 | modules-runtime@0.5.0-modules.8 65 | mongo@1.1.4-modules.8 66 | mongo-id@1.0.1 67 | npm-bcrypt@0.7.8_2 68 | npm-mongo@1.4.40-modules.8 69 | observe-sequence@1.0.8-modules.8 70 | ordered-dict@1.0.4 71 | promise@0.5.2-modules.8 72 | raix:eventemitter@0.1.3 73 | random@1.0.6-modules.8 74 | rate-limit@1.0.1-modules.8 75 | react-meteor-data@0.2.5 76 | reactive-dict@1.1.4-modules.8 77 | reactive-var@1.0.6 78 | reload@1.1.5-modules.8 79 | retry@1.0.4 80 | reywood:publish-composite@1.4.2 81 | routepolicy@1.0.7-modules.8 82 | service-configuration@1.0.6-modules.8 83 | session@1.1.2-modules.8 84 | sha@1.0.4 85 | spacebars@1.0.8-modules.8 86 | spacebars-compiler@1.0.8-modules.8 87 | srp@1.0.5-modules.8 88 | standard-minifiers@1.0.3-modules.8 89 | standard-minifiers-css@1.0.3-modules.8 90 | standard-minifiers-js@1.0.3-modules.8 91 | templating@1.1.6-modules.8 92 | templating-tools@1.0.1-modules.8 93 | tracker@1.0.10-modules.8 94 | ui@1.0.8 95 | underscore@1.0.5-modules.8 96 | url@1.0.6-modules.8 97 | webapp@1.2.4-modules.8 98 | webapp-hashing@1.0.6-modules.8 99 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | **NOTE** this project is deprecated, please instead check out the `react` branch of the Todos app: https://github.com/meteor/todos/tree/react 2 | 3 | 4 | This is a Todos example app built using Meteor 1.3 + React, using ES2015 modules for file organization and NPM for managing React-related dependencies. The server side also follows the principles described in the Meteor Guide. 5 | 6 | ### Running the app 7 | 8 | ``` bash 9 | npm install 10 | meteor 11 | ``` 12 | 13 | ### Scripts 14 | 15 | To lint: 16 | 17 | ``` bash 18 | npm run lint 19 | ``` 20 | -------------------------------------------------------------------------------- /client/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | module-simple-todos-react 4 | 5 | 6 | 7 | 8 | 9 | 10 |
11 | 12 | -------------------------------------------------------------------------------- /client/main.js: -------------------------------------------------------------------------------- 1 | import { render } from 'react-dom'; 2 | import { renderRoutes } from '../imports/startup/client/routes.jsx'; 3 | 4 | Meteor.startup(() => { 5 | render(renderRoutes(), document.getElementById('app')); 6 | }); 7 | -------------------------------------------------------------------------------- /client/main.less: -------------------------------------------------------------------------------- 1 | @import "{}/imports/ui/stylesheets/reset.less"; 2 | 3 | // Global namespace 4 | @import "{}/imports/ui/stylesheets/base.less"; 5 | @import '{}/imports/ui/stylesheets/button.less'; 6 | @import '{}/imports/ui/stylesheets/form.less'; 7 | @import '{}/imports/ui/stylesheets/icon.less'; 8 | @import '{}/imports/ui/stylesheets/link.less'; 9 | @import '{}/imports/ui/stylesheets/nav.less'; 10 | @import '{}/imports/ui/stylesheets/fade-transition.less'; 11 | 12 | // App layout 13 | @import "{}/imports/ui/layouts/App.less"; 14 | 15 | // Pages 16 | @import "{}/imports/ui/pages/ListPage.less"; 17 | @import "{}/imports/ui/pages/AuthPage.less"; 18 | @import "{}/imports/ui/pages/NotFoundPage.less"; 19 | 20 | // Components 21 | @import "{}/imports/ui/components/ConnectionNotification.less"; 22 | @import "{}/imports/ui/components/ListHeader.less"; 23 | @import "{}/imports/ui/components/ListList.less"; 24 | @import "{}/imports/ui/components/Loading.less"; 25 | @import "{}/imports/ui/components/Message.less"; 26 | @import "{}/imports/ui/components/TodoItem.less"; 27 | @import "{}/imports/ui/components/UserMenu.less"; 28 | -------------------------------------------------------------------------------- /imports/api/lists/lists.js: -------------------------------------------------------------------------------- 1 | import { SimpleSchema } from 'meteor/aldeed:simple-schema'; 2 | import { Factory } from 'meteor/factory'; 3 | import { Todos } from '../todos/todos.js'; 4 | 5 | class ListsCollection extends Mongo.Collection { 6 | insert(list, callback) { 7 | if (!list.name) { 8 | let nextLetter = 'A'; 9 | list.name = `List ${nextLetter}`; 10 | 11 | while (!!this.findOne({name: list.name})) { 12 | // not going to be too smart here, can go past Z 13 | nextLetter = String.fromCharCode(nextLetter.charCodeAt(0) + 1); 14 | list.name = `List ${nextLetter}`; 15 | } 16 | } 17 | 18 | return super.insert(list, callback); 19 | } 20 | remove(selector, callback) { 21 | Todos.remove({listId: selector}); 22 | return super.remove(selector, callback); 23 | } 24 | } 25 | 26 | export const Lists = new ListsCollection('Lists'); 27 | 28 | // Deny all client-side updates since we will be using methods to manage this collection 29 | Lists.deny({ 30 | insert() { return true; }, 31 | update() { return true; }, 32 | remove() { return true; }, 33 | }); 34 | 35 | Lists.schema = new SimpleSchema({ 36 | name: { type: String }, 37 | incompleteCount: {type: Number, defaultValue: 0}, 38 | userId: { type: String, regEx: SimpleSchema.RegEx.Id, optional: true } 39 | }); 40 | 41 | Lists.attachSchema(Lists.schema); 42 | 43 | // This represents the keys from Lists objects that should be published 44 | // to the client. If we add secret properties to List objects, don't list 45 | // them here to keep them private to the server. 46 | Lists.publicFields = { 47 | name: 1, 48 | incompleteCount: 1, 49 | userId: 1 50 | }; 51 | 52 | Factory.define('list', Lists, {}); 53 | 54 | Lists.helpers({ 55 | // A list is considered to be private if it has a userId set 56 | isPrivate() { 57 | return !!this.userId; 58 | }, 59 | isLastPublicList() { 60 | const publicListCount = Lists.find({userId: {$exists: false}}).count(); 61 | return !this.isPrivate() && publicListCount === 1; 62 | }, 63 | editableBy(userId) { 64 | if (!this.userId) { 65 | return true; 66 | } 67 | 68 | return this.userId === userId; 69 | }, 70 | todos() { 71 | return Todos.find({listId: this._id}, {sort: {createdAt: -1}}); 72 | } 73 | }); 74 | -------------------------------------------------------------------------------- /imports/api/lists/methods.js: -------------------------------------------------------------------------------- 1 | import { ValidatedMethod } from 'meteor/mdg:validated-method'; 2 | import { SimpleSchema } from 'meteor/aldeed:simple-schema'; 3 | import { DDPRateLimiter } from 'meteor/ddp-rate-limiter'; 4 | import { Lists } from './lists.js'; 5 | 6 | const LIST_ID_ONLY = new SimpleSchema({ 7 | listId: { type: String } 8 | }).validator(); 9 | 10 | export const insert = new ValidatedMethod({ 11 | name: 'Lists.methods.insert', 12 | validate: new SimpleSchema({}).validator(), 13 | run() { 14 | return Lists.insert({}); 15 | } 16 | }); 17 | 18 | export const makePrivate = new ValidatedMethod({ 19 | name: 'Lists.methods.makePrivate', 20 | validate: LIST_ID_ONLY, 21 | run({ listId }) { 22 | if (!this.userId) { 23 | throw new Meteor.Error('Lists.methods.makePrivate.notLoggedIn', 24 | 'Must be logged in to make private lists.'); 25 | } 26 | 27 | const list = Lists.findOne(listId); 28 | 29 | if (list.isLastPublicList()) { 30 | throw new Meteor.Error('Lists.methods.makePrivate.lastPublicList', 31 | 'Cannot make the last public list private.'); 32 | } 33 | 34 | Lists.update(listId, { 35 | $set: { userId: this.userId } 36 | }); 37 | } 38 | }); 39 | 40 | export const makePublic = new ValidatedMethod({ 41 | name: 'Lists.methods.makePublic', 42 | validate: LIST_ID_ONLY, 43 | run({ listId }) { 44 | if (!this.userId) { 45 | throw new Meteor.Error('Lists.methods.makePublic.notLoggedIn', 46 | 'Must be logged in.'); 47 | } 48 | 49 | const list = Lists.findOne(listId); 50 | 51 | if (!list.editableBy(this.userId)) { 52 | throw new Meteor.Error('Lists.methods.makePublic.accessDenied', 53 | 'You don\'t have permission to edit this list.'); 54 | } 55 | 56 | // XXX the security check above is not atomic, so in theory a race condition could 57 | // result in exposing private data 58 | Lists.update(listId, { 59 | $unset: { userId: true } 60 | }); 61 | } 62 | }); 63 | 64 | export const updateName = new ValidatedMethod({ 65 | name: 'Lists.methods.updateName', 66 | validate: new SimpleSchema({ 67 | listId: { type: String }, 68 | newName: { type: String } 69 | }).validator(), 70 | run({ listId, newName }) { 71 | const list = Lists.findOne(listId); 72 | 73 | if (!list.editableBy(this.userId)) { 74 | throw new Meteor.Error('Lists.methods.updateName.accessDenied', 75 | 'You don\'t have permission to edit this list.'); 76 | } 77 | 78 | // XXX the security check above is not atomic, so in theory a race condition could 79 | // result in exposing private data 80 | 81 | Lists.update(listId, { 82 | $set: { name: newName } 83 | }); 84 | } 85 | }); 86 | 87 | export const remove = new ValidatedMethod({ 88 | name: 'Lists.methods.remove', 89 | validate: LIST_ID_ONLY, 90 | run({ listId }) { 91 | const list = Lists.findOne(listId); 92 | 93 | if (!list.editableBy(this.userId)) { 94 | throw new Meteor.Error('Lists.methods.remove.accessDenied', 95 | 'You don\'t have permission to remove this list.'); 96 | } 97 | 98 | // XXX the security check above is not atomic, so in theory a race condition could 99 | // result in exposing private data 100 | 101 | if (list.isLastPublicList()) { 102 | throw new Meteor.Error('Lists.methods.remove.lastPublicList', 103 | 'Cannot delete the last public list.'); 104 | } 105 | 106 | Lists.remove(listId); 107 | } 108 | }); 109 | 110 | // Get list of all method names on Lists 111 | const LISTS_METHODS = _.pluck([ 112 | insert, 113 | makePublic, 114 | makePrivate, 115 | updateName, 116 | remove 117 | ], 'name'); 118 | 119 | if (Meteor.isServer) { 120 | // Only allow 5 list operations per connection per second 121 | DDPRateLimiter.addRule({ 122 | name(name) { 123 | return _.contains(LISTS_METHODS, name); 124 | }, 125 | 126 | // Rate limit per connection ID 127 | connectionId() { return true; } 128 | }, 5, 1000); 129 | } 130 | -------------------------------------------------------------------------------- /imports/api/lists/publications.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable prefer-arrow-callback */ 2 | 3 | import { Lists } from './lists.js'; 4 | 5 | Meteor.publish('Lists.public', function() { 6 | return Lists.find({ 7 | userId: {$exists: false} 8 | }, { 9 | fields: Lists.publicFields 10 | }); 11 | }); 12 | 13 | Meteor.publish('Lists.private', function() { 14 | if (!this.userId) { 15 | return this.ready(); 16 | } 17 | 18 | return Lists.find({ 19 | userId: this.userId 20 | }, { 21 | fields: Lists.publicFields 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /imports/api/todos/incompleteCountDenormalizer.js: -------------------------------------------------------------------------------- 1 | import { Todos } from './todos.js'; 2 | import { Lists } from '../lists/lists.js'; 3 | import { check } from 'meteor/check'; 4 | 5 | const incompleteCountDenormalizer = { 6 | _updateList(listId) { 7 | // Recalculate the correct incomplete count direct from MongoDB 8 | const incompleteCount = Todos.find({ 9 | listId, 10 | checked: false 11 | }).count(); 12 | 13 | Lists.update(listId, {$set: {incompleteCount}}); 14 | }, 15 | afterInsertTodo(todo) { 16 | this._updateList(todo.listId); 17 | }, 18 | afterUpdateTodo(selector, modifier) { 19 | // We only support very limited operations on todos 20 | check(modifier, {$set: Object}); 21 | 22 | // We can only deal with $set modifiers, but that's all we do in this app 23 | if (_.has(modifier.$set, 'checked')) { 24 | Todos.find(selector, {fields: {listId: 1}}).forEach(todo => { 25 | this._updateList(todo.listId); 26 | }); 27 | } 28 | }, 29 | // Here we need to take the list of todos being removed, selected *before* the update 30 | // because otherwise we can't figure out the relevant list id(s) (if the todo has been deleted) 31 | afterRemoveTodos(todos) { 32 | todos.forEach(todo => this._updateList(todo.listId)); 33 | } 34 | }; 35 | 36 | export default incompleteCountDenormalizer; 37 | -------------------------------------------------------------------------------- /imports/api/todos/methods.js: -------------------------------------------------------------------------------- 1 | import { Todos } from './todos.js'; 2 | import { Lists } from '../lists/lists.js'; 3 | import { ValidatedMethod } from 'meteor/mdg:validated-method'; 4 | import { SimpleSchema } from 'meteor/aldeed:simple-schema'; 5 | import { DDPRateLimiter } from 'meteor/ddp-rate-limiter'; 6 | 7 | export const insert = new ValidatedMethod({ 8 | name: 'Todos.methods.insert', 9 | validate: new SimpleSchema({ 10 | listId: { type: String }, 11 | text: { type: String } 12 | }).validator(), 13 | run({ listId, text }) { 14 | const list = Lists.findOne(listId); 15 | 16 | if (list.isPrivate() && list.userId !== this.userId) { 17 | throw new Meteor.Error('Todos.methods.insert.unauthorized', 18 | 'Cannot add todos to a private list that is not yours'); 19 | } 20 | 21 | const todo = { 22 | listId, 23 | text, 24 | checked: false, 25 | createdAt: new Date() 26 | }; 27 | 28 | Todos.insert(todo); 29 | } 30 | }); 31 | 32 | export const setCheckedStatus = new ValidatedMethod({ 33 | name: 'Todos.methods.makeChecked', 34 | validate: new SimpleSchema({ 35 | todoId: { type: String }, 36 | newCheckedStatus: { type: Boolean } 37 | }).validator(), 38 | run({ todoId, newCheckedStatus }) { 39 | const todo = Todos.findOne(todoId); 40 | 41 | if (todo.checked === newCheckedStatus) { 42 | // The status is already what we want, let's not do any extra work 43 | return; 44 | } 45 | 46 | if (!todo.editableBy(this.userId)) { 47 | throw new Meteor.Error('Todos.methods.setCheckedStatus.unauthorized', 48 | 'Cannot edit checked status in a private list that is not yours'); 49 | } 50 | 51 | Todos.update(todoId, {$set: { 52 | checked: newCheckedStatus 53 | }}); 54 | } 55 | }); 56 | 57 | export const updateText = new ValidatedMethod({ 58 | name: 'Todos.methods.updateText', 59 | validate: new SimpleSchema({ 60 | todoId: { type: String }, 61 | newText: { type: String } 62 | }).validator(), 63 | run({ todoId, newText }) { 64 | // This is complex auth stuff - perhaps denormalizing a userId onto todos 65 | // would be correct here? 66 | const todo = Todos.findOne(todoId); 67 | 68 | if (!todo.editableBy(this.userId)) { 69 | throw new Meteor.Error('Todos.methods.updateText.unauthorized', 70 | 'Cannot edit todos in a private list that is not yours'); 71 | } 72 | 73 | Todos.update(todoId, { 74 | $set: { text: newText } 75 | }); 76 | } 77 | }); 78 | 79 | export const remove = new ValidatedMethod({ 80 | name: 'Todos.methods.remove', 81 | validate: new SimpleSchema({ 82 | todoId: { type: String } 83 | }).validator(), 84 | run({ todoId }) { 85 | const todo = Todos.findOne(todoId); 86 | 87 | if (!todo.editableBy(this.userId)) { 88 | throw new Meteor.Error('Todos.methods.remove.unauthorized', 89 | 'Cannot remove todos in a private list that is not yours'); 90 | } 91 | 92 | Todos.remove(todoId); 93 | } 94 | }); 95 | 96 | // Get list of all method names on Todos 97 | const TODOS_METHODS = _.pluck([ 98 | insert, 99 | setCheckedStatus, 100 | updateText, 101 | remove, 102 | ], 'name'); 103 | 104 | if (Meteor.isServer) { 105 | // Only allow 5 todos operations per connection per second 106 | DDPRateLimiter.addRule({ 107 | name(name) { 108 | return _.contains(TODOS_METHODS, name); 109 | }, 110 | 111 | // Rate limit per connection ID 112 | connectionId() { return true; } 113 | }, 5, 1000); 114 | } 115 | -------------------------------------------------------------------------------- /imports/api/todos/publications.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable prefer-arrow-callback */ 2 | 3 | import { Todos } from './todos.js'; 4 | import { Lists } from '../lists/lists.js'; 5 | import { SimpleSchema } from 'meteor/aldeed:simple-schema'; 6 | 7 | Meteor.publishComposite('Todos.inList', function(listId) { 8 | new SimpleSchema({ 9 | listId: {type: String} 10 | }).validate({ listId }); 11 | 12 | const userId = this.userId; 13 | 14 | return { 15 | find() { 16 | const query = { 17 | _id: listId, 18 | $or: [{userId: {$exists: false}}, {userId}] 19 | }; 20 | 21 | // We only need the _id field in this query, since it's only 22 | // used to drive the child queries to get the todos 23 | const options = { 24 | fields: { _id: 1 } 25 | }; 26 | 27 | return Lists.find(query, options); 28 | }, 29 | 30 | children: [{ 31 | find(list) { 32 | return Todos.find({ listId: list._id }, { 33 | fields: Todos.publicFields 34 | }); 35 | } 36 | }] 37 | }; 38 | }); 39 | -------------------------------------------------------------------------------- /imports/api/todos/todos.js: -------------------------------------------------------------------------------- 1 | import incompleteCountDenormalizer from './incompleteCountDenormalizer.js'; 2 | import { SimpleSchema } from 'meteor/aldeed:simple-schema'; 3 | import { Factory } from 'meteor/factory'; 4 | import { faker } from 'meteor/dfischer:faker'; 5 | import { Lists } from '../lists/lists.js'; 6 | 7 | class TodosCollection extends Mongo.Collection { 8 | insert(doc, callback) { 9 | doc.createdAt = doc.createdAt || new Date(); 10 | const result = super.insert(doc, callback); 11 | incompleteCountDenormalizer.afterInsertTodo(doc); 12 | return result; 13 | } 14 | update(selector, modifier) { 15 | const result = super.update(selector, modifier); 16 | incompleteCountDenormalizer.afterUpdateTodo(selector, modifier); 17 | return result; 18 | } 19 | remove(selector) { 20 | const todos = this.find(selector).fetch(); 21 | const result = super.remove(selector); 22 | incompleteCountDenormalizer.afterRemoveTodos(todos); 23 | return result; 24 | } 25 | } 26 | 27 | export const Todos = new TodosCollection('Todos'); 28 | 29 | // Deny all client-side updates since we will be using methods to manage this collection 30 | Todos.deny({ 31 | insert() { return true; }, 32 | update() { return true; }, 33 | remove() { return true; }, 34 | }); 35 | 36 | Todos.schema = new SimpleSchema({ 37 | listId: { 38 | type: String, 39 | regEx: SimpleSchema.RegEx.Id, 40 | denyUpdate: true 41 | }, 42 | text: { 43 | type: String, 44 | max: 100 45 | }, 46 | createdAt: { 47 | type: Date, 48 | denyUpdate: true 49 | }, 50 | checked: { 51 | type: Boolean, 52 | defaultValue: false 53 | } 54 | }); 55 | 56 | Todos.attachSchema(Todos.schema); 57 | 58 | // This represents the keys from Lists objects that should be published 59 | // to the client. If we add secret properties to List objects, don't list 60 | // them here to keep them private to the server. 61 | Todos.publicFields = { 62 | listId: 1, 63 | text: 1, 64 | createdAt: 1, 65 | checked: 1 66 | }; 67 | 68 | // TODO This factory has a name - do we have a code style for this? 69 | // - usually I've used the singular, sometimes you have more than one though, like 70 | // 'todo', 'emptyTodo', 'checkedTodo' 71 | Factory.define('todo', Todos, { 72 | listId: () => Factory.get('list'), 73 | text: () => faker.lorem.sentence(), 74 | createdAt: () => new Date() 75 | }); 76 | 77 | Todos.helpers({ 78 | list() { 79 | return Lists.findOne(this.listId); 80 | }, 81 | editableBy(userId) { 82 | return this.list().editableBy(userId); 83 | } 84 | }); 85 | -------------------------------------------------------------------------------- /imports/startup/client/routes.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Router, Route, browserHistory } from 'react-router'; 3 | 4 | // route components 5 | import AppContainer from '../../ui/containers/AppContainer.jsx'; 6 | import ListContainer from '../../ui/containers/ListContainer.jsx'; 7 | import AuthPageSignIn from '../../ui/pages/AuthPageSignIn.jsx'; 8 | import AuthPageJoin from '../../ui/pages/AuthPageJoin.jsx'; 9 | import NotFoundPage from '../../ui/pages/NotFoundPage.jsx'; 10 | 11 | export const renderRoutes = () => ( 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | ); 21 | -------------------------------------------------------------------------------- /imports/startup/server/fixtures.js: -------------------------------------------------------------------------------- 1 | import { Lists } from '../../api/lists/lists.js'; 2 | import { Todos } from '../../api/todos/todos.js'; 3 | 4 | // if the database is empty on server start, create some sample data. 5 | Meteor.startup(() => { 6 | if (Lists.find().count() === 0) { 7 | const data = [ 8 | { 9 | name: 'Meteor Principles', 10 | items: [ 11 | 'Data on the Wire', 12 | 'One Language', 13 | 'Database Everywhere', 14 | 'Latency Compensation', 15 | 'Full Stack Reactivity', 16 | 'Embrace the Ecosystem', 17 | 'Simplicity Equals Productivity' 18 | ] 19 | }, 20 | { 21 | name: 'Languages', 22 | items: [ 23 | 'Lisp', 24 | 'C', 25 | 'C++', 26 | 'Python', 27 | 'Ruby', 28 | 'JavaScript', 29 | 'Scala', 30 | 'Erlang', 31 | '6502 Assembly' 32 | ] 33 | }, 34 | { 35 | name: 'Favorite Scientists', 36 | items: [ 37 | 'Ada Lovelace', 38 | 'Grace Hopper', 39 | 'Marie Curie', 40 | 'Carl Friedrich Gauss', 41 | 'Nikola Tesla', 42 | 'Claude Shannon' 43 | ] 44 | } 45 | ]; 46 | 47 | let timestamp = (new Date()).getTime(); 48 | 49 | data.forEach((list) => { 50 | const listId = Lists.insert({ 51 | name: list.name, 52 | incompleteCount: list.items.length 53 | }); 54 | 55 | list.items.forEach((text) => { 56 | Todos.insert({ 57 | listId: listId, 58 | text: text, 59 | createdAt: new Date(timestamp) 60 | }); 61 | 62 | timestamp += 1; // ensure unique timestamp. 63 | }); 64 | }); 65 | } 66 | }); 67 | -------------------------------------------------------------------------------- /imports/startup/server/register-api.js: -------------------------------------------------------------------------------- 1 | import '../../api/todos/methods.js'; 2 | import '../../api/todos/publications.js'; 3 | import '../../api/lists/methods.js'; 4 | import '../../api/lists/publications.js'; 5 | -------------------------------------------------------------------------------- /imports/startup/server/security.js: -------------------------------------------------------------------------------- 1 | import { DDPRateLimiter } from 'meteor/ddp-rate-limiter'; 2 | 3 | // Don't let people write arbitrary data to their 'profile' field from the client 4 | Meteor.users.deny({ 5 | update() { 6 | return true; 7 | } 8 | }); 9 | 10 | // Get a list of all accounts methods by running `Meteor.server.method_handlers` in meteor shell 11 | const AUTH_METHODS = [ 12 | 'login', 13 | 'logout', 14 | 'logoutOtherClients', 15 | 'getNewToken', 16 | 'removeOtherTokens', 17 | 'configureLoginService', 18 | 'changePassword', 19 | 'forgotPassword', 20 | 'resetPassword', 21 | 'verifyEmail', 22 | 'createUser', 23 | 'ATRemoveService', 24 | 'ATCreateUserServer', 25 | 'ATResendVerificationEmail', 26 | ]; 27 | 28 | if (Meteor.isServer) { 29 | // Only allow 2 login attempts per connection per 5 seconds 30 | DDPRateLimiter.addRule({ 31 | name(name) { 32 | return _.contains(AUTH_METHODS, name); 33 | }, 34 | 35 | // Rate limit per connection ID 36 | connectionId() { return true; } 37 | }, 2, 5000); 38 | } 39 | -------------------------------------------------------------------------------- /imports/ui/components/ConnectionNotification.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const ConnectionNotification = () => ( 4 |
5 |
6 | 7 |
8 |
Trying to connect
9 |
There seems to be a connection issue
10 |
11 |
12 |
13 | ); 14 | 15 | export default ConnectionNotification; 16 | -------------------------------------------------------------------------------- /imports/ui/components/ConnectionNotification.less: -------------------------------------------------------------------------------- 1 | @import '{}/imports/ui/stylesheets/utils.less'; 2 | 3 | @-webkit-keyframes spin { 4 | 0% { transform: rotate(0deg); } 5 | 100% { transform: rotate(359deg); } 6 | } 7 | @keyframes spin { 8 | 0% { transform: rotate(0deg); } 9 | 100% { transform: rotate(359deg); } 10 | } 11 | 12 | 13 | // Notification message (e.g., when unable to connect) 14 | .notifications { 15 | .position(absolute, auto, auto, 10px, 50%, 280px); 16 | transform: translate3d(-50%, 0, 0); 17 | z-index: 1; 18 | 19 | @media screen and (min-width: 40em) { 20 | transform: translate3d(0, 0, 0); 21 | bottom: auto; 22 | right: 1rem; 23 | top: 1rem; 24 | left: auto; 25 | } 26 | 27 | .notification { 28 | .font-s1; 29 | background: rgba(51,51,51, .85); 30 | color: @color-empty; 31 | margin-bottom: .25rem; 32 | padding: .5rem .75rem; 33 | position: relative; 34 | width: 100%; 35 | 36 | .icon-sync { 37 | .position(absolute, 30%, auto, auto, 1rem); 38 | animation: spin 2s infinite linear; 39 | color: @color-empty; 40 | font-size: 1.5em; 41 | } 42 | 43 | .meta { 44 | overflow: hidden; 45 | padding-left: 3em; 46 | 47 | .title-notification { 48 | .title-caps; 49 | display: block; 50 | } 51 | 52 | .description { 53 | display: block; 54 | } 55 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /imports/ui/components/ListHeader.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import MobileMenu from './MobileMenu.jsx'; 3 | 4 | import { 5 | updateName, 6 | makePublic, 7 | makePrivate, 8 | remove, 9 | } from '../../api/lists/methods.js'; 10 | 11 | import { 12 | insert, 13 | } from '../../api/todos/methods.js'; 14 | 15 | export default class ListHeader extends React.Component { 16 | constructor(props) { 17 | super(props); 18 | this.state = { editing: false }; 19 | } 20 | 21 | editList() { 22 | this.setState({ editing: true }, () => { 23 | this.refs.listNameInput.focus(); 24 | }); 25 | } 26 | 27 | cancelEdit() { 28 | this.setState({ editing: false }); 29 | } 30 | 31 | saveList() { 32 | this.setState({ editing: false }); 33 | updateName.call({ 34 | listId: this.props.list._id, 35 | newName: this.refs.listNameInput.value 36 | }, (err) => { 37 | /* eslint-disable no-console */ 38 | err && console.error(err); 39 | /* eslint-enable no-console */ 40 | }); 41 | } 42 | 43 | deleteList() { 44 | const list = this.props.list; 45 | const message = `Are you sure you want to delete the list ${list.name}?`; 46 | 47 | if (confirm(message)) { 48 | remove.call({ 49 | listId: list._id 50 | }, (err) => { 51 | err && alert(err.error); 52 | }); 53 | this.context.router.push('/'); 54 | } 55 | } 56 | 57 | toggleListPrivacy() { 58 | const list = this.props.list; 59 | if (list.userId) { 60 | makePublic.call({ listId: list._id }, (err) => { 61 | err && alert(err.error); 62 | }); 63 | } else { 64 | makePrivate.call({ listId: list._id }, (err) => { 65 | err && alert(err.error); 66 | }); 67 | } 68 | } 69 | 70 | onListFormSubmit(event) { 71 | event.preventDefault(); 72 | this.saveList(); 73 | } 74 | 75 | onListInputKeyUp(event) { 76 | if (event.keyCode === 27) { 77 | this.cancelEdit(); 78 | } 79 | } 80 | 81 | onListInputBlur() { 82 | if (this.state.editing) { 83 | this.saveList(); 84 | } 85 | } 86 | 87 | onListDropdownAction(event) { 88 | if (event.target.value === 'delete') { 89 | this.deleteList(); 90 | } else { 91 | this.toggleListPrivacy(); 92 | } 93 | } 94 | 95 | createTodo(event) { 96 | event.preventDefault(); 97 | const input = this.refs.newTodoInput; 98 | if (input.value.trim()) { 99 | insert.call({ 100 | listId: this.props.list._id, 101 | text: input.value 102 | }, (err) => { 103 | err && alert(err.error); 104 | }); 105 | input.value = ''; 106 | } 107 | } 108 | 109 | focusTodoInput() { 110 | this.refs.newTodoInput.focus(); 111 | } 112 | 113 | renderDefaultHeader() { 114 | const { list } = this.props; 115 | return ( 116 |
117 | 118 |

119 | {list.name} 120 | {list.incompleteCount} 121 |

122 |
123 |
124 | 133 | 134 |
135 |
136 | 137 | {list.userId 138 | ? 139 | : } 140 | 141 | 142 | 143 | 144 |
145 |
146 |
147 | ); 148 | } 149 | 150 | renderEditingHeader() { 151 | const { list } = this.props; 152 | return ( 153 |
154 | 161 |
162 | 165 | 166 | 167 |
168 |
169 | ); 170 | } 171 | 172 | render() { 173 | const { editing } = this.state; 174 | return ( 175 | 182 | ); 183 | } 184 | } 185 | 186 | ListHeader.propTypes = { 187 | list: React.PropTypes.object 188 | }; 189 | 190 | ListHeader.contextTypes = { 191 | router: React.PropTypes.object 192 | }; 193 | -------------------------------------------------------------------------------- /imports/ui/components/ListHeader.less: -------------------------------------------------------------------------------- 1 | @import '{}/imports/ui/stylesheets/utils.less'; 2 | 3 | .list-header { 4 | background: linear-gradient(to bottom, #d0edf5, #e1e5f0 100%); 5 | height: 5em; 6 | 7 | text-align: center; 8 | @media screen and (min-width: 40em) { text-align: left; } 9 | 10 | .title-page { 11 | .position(absolute, 0, 3rem, auto, 3rem); 12 | line-height: 3rem; 13 | @media screen and (min-width: 40em) { 14 | left: 1rem; 15 | right: 6rem; 16 | } 17 | 18 | cursor: pointer; 19 | font-size: 1.125em; // 18px 20 | white-space: nowrap; 21 | 22 | .title-wrapper { 23 | .ellipsized; 24 | color: @color-ancillary; 25 | display: inline-block; 26 | padding-right: 1.5rem; 27 | vertical-align: top; 28 | max-width: 100%; 29 | } 30 | 31 | .count-list { 32 | background: @color-primary; 33 | border-radius: 1em; 34 | color: @color-empty; 35 | display: inline-block; 36 | font-size: .7rem; 37 | line-height: 1; 38 | margin-left: -1.25rem; 39 | margin-top: -4px; 40 | padding: .3em .5em; 41 | vertical-align: middle; 42 | } 43 | } 44 | form.todo-new { 45 | .position(absolute, 3em, 0, auto, 0); 46 | 47 | input[type="text"] { 48 | background: transparent; 49 | padding-bottom: .25em; 50 | padding-left: 44px !important; 51 | padding-top: .25em; 52 | } 53 | } 54 | form.list-edit-form { 55 | position: relative; 56 | 57 | input[type="text"] { 58 | background: transparent; 59 | font-size: 1.125em; // 18px 60 | width: 100%; 61 | padding-right: 3em; 62 | padding-left: 1rem; 63 | } 64 | } 65 | 66 | select.list-edit { 67 | .font-s2; 68 | .position(absolute, 0,0,0,0); 69 | background: transparent; 70 | opacity: 0; // allows the cog to appear 71 | } 72 | 73 | .options-web { 74 | display: none; 75 | 76 | .nav-item { 77 | .font-s3; 78 | width: 2rem; 79 | 80 | &:last-child { margin-right: .5rem; } 81 | } 82 | } 83 | 84 | // Hide & show options and nav icons 85 | @media screen and (min-width: 40em) { 86 | .nav-group:not(.right) { display: none !important; } 87 | .options-mobile { display: none; } 88 | .options-web { display: block; } 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /imports/ui/components/ListList.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Link } from 'react-router'; 3 | import { insert } from '../../api/lists/methods.js'; 4 | 5 | export default class ListList extends React.Component { 6 | createNewList() { 7 | const { router } = this.context; 8 | const listId = insert.call((err) => { 9 | if (err) { 10 | router.push('/'); 11 | /* eslint-disable no-alert */ 12 | alert('Could not create list.'); 13 | } 14 | }); 15 | router.push(`/lists/${ listId }`); 16 | } 17 | 18 | render() { 19 | const { lists } = this.props; 20 | return ( 21 |
22 | 23 | 24 | New List 25 | 26 | {lists.map(list => ( 27 | 33 | {list.userId 34 | ? 35 | : null} 36 | {list.incompleteCount 37 | ? {list.incompleteCount} 38 | : null} 39 | {list.name} 40 | 41 | ))} 42 |
43 | ); 44 | } 45 | } 46 | 47 | ListList.propTypes = { 48 | lists: React.PropTypes.array 49 | }; 50 | 51 | ListList.contextTypes = { 52 | router: React.PropTypes.object 53 | }; 54 | -------------------------------------------------------------------------------- /imports/ui/components/ListList.less: -------------------------------------------------------------------------------- 1 | @import '{}/imports/ui/stylesheets/utils.less'; 2 | 3 | .list-todos { 4 | a { 5 | box-shadow: rgba(255,255,255,.15) 0 1px 0 0; 6 | display: block; 7 | line-height: 1.5em; 8 | padding: .75em 2.5em; 9 | position: relative; 10 | } 11 | 12 | .count-list { 13 | transition: all 200ms ease-in; 14 | background: rgba(255,255,255,.1); 15 | border-radius: 1em; 16 | float: right; 17 | font-size: .7rem; 18 | line-height: 1; 19 | margin-top: .25rem; 20 | margin-right: -1.5em; 21 | padding: .3em .5em; 22 | } 23 | 24 | [class^="icon-"], 25 | [class*=" icon-"] { 26 | .font-s2; 27 | float: left; 28 | margin-left: -1.5rem; 29 | margin-right: .5rem; 30 | margin-top: .1rem; 31 | width: 1em; 32 | } 33 | 34 | .icon-lock { 35 | .font-s1; 36 | margin-top: .2rem; 37 | opacity: .8; 38 | } 39 | 40 | .list-todo { 41 | color: rgba(255,255,255,.4); 42 | 43 | &:hover, 44 | &:active, 45 | &.active { 46 | color: @color-empty; 47 | .count-list { background: @color-primary; } 48 | } 49 | 50 | .cordova &:hover { 51 | // Prevent hover states from being noticeable on Cordova apps 52 | color: rgba(255,255,255,.4); 53 | } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /imports/ui/components/Loading.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const Loading = () => ( 4 | 5 | ); 6 | 7 | export default Loading; 8 | -------------------------------------------------------------------------------- /imports/ui/components/Loading.less: -------------------------------------------------------------------------------- 1 | @import '{}/imports/ui/stylesheets/utils.less'; 2 | 3 | .loading-app { 4 | .position(absolute, 50%, 50%, auto, auto, 50%); 5 | transform: translate3d(50%, -50%, 0); 6 | min-width: 160px; 7 | max-width: 320px; 8 | } 9 | -------------------------------------------------------------------------------- /imports/ui/components/Message.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const Message = ({ title, subtitle }) => ( 4 |
5 | {title ?
{title}
: null} 6 | {subtitle ?
{subtitle}
: null} 7 |
8 | ); 9 | 10 | Message.propTypes = { 11 | title: React.PropTypes.string, 12 | subtitle: React.PropTypes.string 13 | }; 14 | 15 | export default Message; 16 | -------------------------------------------------------------------------------- /imports/ui/components/Message.less: -------------------------------------------------------------------------------- 1 | @import '{}/imports/ui/stylesheets/utils.less'; 2 | 3 | // Empty states and 404 messages 4 | .wrapper-message { 5 | .position(absolute, 45%, 0, auto, 0); 6 | transform: translate3d(0, -50%, 0); 7 | text-align: center; 8 | 9 | .title-message { 10 | .font-m2; 11 | .type-light; 12 | color: @color-ancillary; 13 | margin-bottom: .5em; 14 | } 15 | 16 | .subtitle-message { 17 | .font-s2; 18 | color: @color-medium; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /imports/ui/components/MobileMenu.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | function toggleMenu() { 4 | Session.set('menuOpen', !Session.get('menuOpen')); 5 | } 6 | 7 | const MobileMenu = () => ( 8 |
9 | 10 | 11 | 12 |
13 | ); 14 | 15 | export default MobileMenu; 16 | -------------------------------------------------------------------------------- /imports/ui/components/TodoItem.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import classnames from 'classnames'; 3 | 4 | import { 5 | setCheckedStatus, 6 | updateText, 7 | remove, 8 | } from '../../api/todos/methods.js'; 9 | 10 | export default class TodoItem extends React.Component { 11 | constructor(props) { 12 | super(props); 13 | this.throttledUpdate = _.throttle(value => { 14 | if (value) { 15 | updateText.call({ 16 | todoId: this.props.todo._id, 17 | newText: value 18 | }, (err) => { 19 | err && alert(err.error); 20 | }); 21 | } 22 | }, 300); 23 | } 24 | 25 | setTodoCheckStatus(event) { 26 | setCheckedStatus.call({ 27 | todoId: this.props.todo._id, 28 | newCheckedStatus: event.target.checked 29 | }); 30 | } 31 | 32 | updateTodo(event) { 33 | this.throttledUpdate(event.target.value); 34 | } 35 | 36 | deleteTodo() { 37 | remove.call({ 38 | todoId: this.props.todo._id 39 | }, (err) => { 40 | err && alert(err.error); 41 | }); 42 | } 43 | 44 | onFocus() { 45 | this.props.onEditingChange(this.props.todo._id, true); 46 | } 47 | 48 | onBlur() { 49 | this.props.onEditingChange(this.props.todo._id, false); 50 | } 51 | 52 | render() { 53 | const { todo, editing } = this.props; 54 | const todoClass = classnames({ 55 | 'list-item': true, 56 | checked: todo.checked, 57 | editing 58 | }); 59 | 60 | return ( 61 |
62 | 70 | 77 | 82 | 83 | 84 |
85 | ); 86 | } 87 | } 88 | 89 | TodoItem.propTypes = { 90 | todo: React.PropTypes.object, 91 | editing: React.PropTypes.bool 92 | }; 93 | -------------------------------------------------------------------------------- /imports/ui/components/TodoItem.less: -------------------------------------------------------------------------------- 1 | @import '{}/imports/ui/stylesheets/utils.less'; 2 | 3 | .list-items .list-item { 4 | .font-s2; 5 | 6 | // Layout of list-item children 7 | display: flex; 8 | flex-wrap: wrap; 9 | height: 3rem; 10 | width: 100%; 11 | 12 | .checkbox { 13 | flex: 0, 0, 44px; 14 | cursor: pointer; 15 | } 16 | input[type="text"] { flex: 1; } 17 | .delete-item { flex: 0 0 3rem; } 18 | 19 | 20 | // Style of list-item children 21 | input[type="text"] { 22 | background: transparent; 23 | cursor: pointer; 24 | 25 | &:focus { cursor: text; } 26 | } 27 | 28 | .delete-item { 29 | color: @color-medium-rare; 30 | line-height: 3rem; 31 | text-align: center; 32 | 33 | &:hover { color: @color-primary; } 34 | &:active { color: @color-well; } 35 | .icon-trash { font-size: 1.1em; } 36 | } 37 | 38 | 39 | // Border between list items 40 | & + .list-item { border-top: 1px solid #f0f9fb; } 41 | 42 | // Checked 43 | &.checked { 44 | input[type="text"] { 45 | color: @color-medium-rare; 46 | text-decoration: line-through; 47 | } 48 | 49 | .delete-item { display: inline-block; } 50 | } 51 | 52 | // Editing 53 | .delete-item { display: none; } 54 | &.editing .delete-item { display: inline-block; } 55 | } 56 | -------------------------------------------------------------------------------- /imports/ui/components/UserMenu.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Link } from 'react-router'; 3 | 4 | export default class UserMenu extends React.Component { 5 | constructor(props) { 6 | super(props); 7 | this.state = { 8 | open: false 9 | }; 10 | } 11 | 12 | toggle(e) { 13 | e.stopPropagation(); 14 | this.setState({ 15 | open: !this.state.open 16 | }); 17 | } 18 | 19 | renderLoggedIn() { 20 | const { open } = this.state; 21 | const { user, logout } = this.props; 22 | const email = user.emails[0].address; 23 | const emailLocalPart = email.substring(0, email.indexOf('@')); 24 | 25 | return ( 26 |
27 | 28 | {open 29 | ? 30 | : } 31 | {emailLocalPart} 32 | 33 | {open 34 | ? Logout 35 | : null} 36 |
37 | ); 38 | } 39 | 40 | renderLoggedOut() { 41 | return ( 42 |
43 | Sign In 44 | Join 45 |
46 | ); 47 | } 48 | 49 | render() { 50 | return this.props.user 51 | ? this.renderLoggedIn() 52 | : this.renderLoggedOut(); 53 | } 54 | } 55 | 56 | UserMenu.propTypes = { 57 | user: React.PropTypes.object, 58 | logout: React.PropTypes.func 59 | }; 60 | -------------------------------------------------------------------------------- /imports/ui/components/UserMenu.less: -------------------------------------------------------------------------------- 1 | @import '{}/imports/ui/stylesheets/utils.less'; 2 | 3 | .user-menu { 4 | margin: 2em auto 2em; 5 | width: 80%; 6 | 7 | .btn-secondary { 8 | .font-s1; 9 | padding-top: .5em; 10 | padding-bottom: .5em; 11 | } 12 | 13 | &.vertical .btn-secondary { 14 | .force-wrap; 15 | padding-right: 2.5em; 16 | text-align: left; 17 | text-indent: 0; 18 | white-space: normal; // Resets wrapping 19 | width: 100%; 20 | 21 | & + .btn-secondary { 22 | margin-top: .5rem; 23 | 24 | &:before { 25 | .position(absolute, -.5rem, 50%, auto, auto, 1px, .5rem); 26 | background: lighten(#517096, 5%); 27 | content: ''; 28 | } 29 | } 30 | 31 | [class^="icon-"], 32 | [class*=" icon-"] { 33 | .position(absolute, .5em, .5em, auto, auto); 34 | line-height: 20px; 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /imports/ui/containers/AppContainer.jsx: -------------------------------------------------------------------------------- 1 | import { Lists } from '../../api/lists/lists.js'; 2 | import { createContainer } from '../helpers/create-container.jsx'; 3 | import App from '../layouts/App.jsx'; 4 | 5 | export default createContainer(() => { 6 | const publicHandle = Meteor.subscribe('Lists.public'); 7 | const privateHandle = Meteor.subscribe('Lists.private'); 8 | return { 9 | user: Meteor.user(), 10 | loading: !(publicHandle.ready() && privateHandle.ready()), 11 | connected: Meteor.status().connected, 12 | menuOpen: Session.get('menuOpen'), 13 | lists: Lists.find({$or: [ 14 | {userId: {$exists: false}}, 15 | {userId: Meteor.userId()} 16 | ]}).fetch() 17 | }; 18 | }, App); 19 | -------------------------------------------------------------------------------- /imports/ui/containers/ListContainer.jsx: -------------------------------------------------------------------------------- 1 | import { Lists } from '../../api/lists/lists.js'; 2 | import { createContainer } from '../helpers/create-container.jsx'; 3 | import ListPage from '../pages/ListPage.jsx'; 4 | 5 | export default createContainer(({ params: { id }}) => { 6 | const todosHandle = Meteor.subscribe('Todos.inList', id); 7 | const loading = !todosHandle.ready(); 8 | const list = Lists.findOne(id); 9 | const listExists = !loading && !!list; 10 | return { 11 | loading, 12 | list, 13 | listExists, 14 | todos: listExists ? list.todos().fetch() : [] 15 | }; 16 | }, ListPage); 17 | -------------------------------------------------------------------------------- /imports/ui/helpers/create-container-with-komposer.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Helper method for easier creation of meteor data containers 3 | * with react-komposer. WIP. There are some currently some weird 4 | * subscription issues for the initial render. 5 | */ 6 | 7 | import { composeWithTracker } from 'react-komposer'; 8 | 9 | export function createContainer(options = {}, Component) { 10 | if (typeof options === 'function') { 11 | options = { 12 | getMeteorData: options 13 | } 14 | } 15 | 16 | const { 17 | getMeteorData, 18 | loadingComponent = null, 19 | errorComponent = null, 20 | pure = true 21 | } = options; 22 | 23 | const compose = (props, onData) => onData(null, getMeteorData(props)); 24 | 25 | return composeWithTracker( 26 | compose, 27 | loadingComponent, 28 | errorComponent, 29 | { pure } 30 | )(Component); 31 | } 32 | -------------------------------------------------------------------------------- /imports/ui/helpers/create-container.jsx: -------------------------------------------------------------------------------- 1 | /** 2 | * Container helper using react-meteor-data. 3 | */ 4 | 5 | import React from 'react'; 6 | import { ReactMeteorData } from 'meteor/react-meteor-data'; 7 | import PureRenderMixin from 'react-addons-pure-render-mixin'; 8 | 9 | export function createContainer(options = {}, Component) { 10 | if (typeof options === 'function') { 11 | options = { 12 | getMeteorData: options 13 | } 14 | } 15 | 16 | const { 17 | getMeteorData, 18 | pure = true 19 | } = options; 20 | 21 | const mixins = [ReactMeteorData]; 22 | if (pure) { 23 | mixins.push(PureRenderMixin); 24 | } 25 | 26 | /* eslint-disable react/prefer-es6-class */ 27 | return React.createClass({ 28 | displayName: 'MeteorDataContainer', 29 | mixins, 30 | getMeteorData() { 31 | return getMeteorData(this.props); 32 | }, 33 | render() { 34 | return ; 35 | } 36 | }); 37 | } 38 | -------------------------------------------------------------------------------- /imports/ui/layouts/App.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactCSSTransitionGroup from 'react-addons-css-transition-group'; 3 | import { Lists } from '../../api/lists/lists.js'; 4 | import UserMenu from '../components/UserMenu.jsx'; 5 | import ListList from '../components/ListList.jsx'; 6 | import ConnectionNotification from '../components/ConnectionNotification.jsx'; 7 | import Loading from '../components/Loading.jsx'; 8 | 9 | const CONNECTION_ISSUE_TIMEOUT = 5000; 10 | 11 | export default class App extends React.Component { 12 | constructor(props) { 13 | super(props); 14 | this.state = { 15 | menuOpen: false, 16 | showConnectionIssue: false 17 | }; 18 | } 19 | 20 | componentWillReceiveProps({ loading, children }) { 21 | // redirect / to a list once lists are ready 22 | if (!loading && !children) { 23 | const list = Lists.findOne(); 24 | this.context.router.replace(`/lists/${ list._id }`); 25 | } 26 | } 27 | 28 | componentDidMount() { 29 | setTimeout(() => { 30 | /* eslint-disable react/no-did-mount-set-state */ 31 | this.setState({ showConnectionIssue: true }); 32 | }, CONNECTION_ISSUE_TIMEOUT); 33 | } 34 | 35 | toggleMenu(menuOpen = !Session.get('menuOpen')) { 36 | Session.set({ menuOpen }); 37 | } 38 | 39 | logout() { 40 | Meteor.logout(); 41 | 42 | // if we are on a private list, we'll need to go to a public one 43 | const list = Lists.findOne(this.props.params.id); 44 | if (list.userId) { 45 | const publicList = Lists.findOne({ userId: { $exists: false }}); 46 | this.context.router.push(`/lists/${ publicList._id }`); 47 | } 48 | } 49 | 50 | render() { 51 | const { showConnectionIssue } = this.state; 52 | const { 53 | user, 54 | connected, 55 | loading, 56 | lists, 57 | menuOpen, 58 | children, 59 | location 60 | } = this.props; 61 | 62 | const closeMenu = this.toggleMenu.bind(this, false); 63 | 64 | // clone route components with keys so that they can 65 | // have transitions 66 | const clonedChildren = children && React.cloneElement(children, { 67 | key: location.pathname 68 | }); 69 | 70 | return ( 71 |
72 | 76 | {showConnectionIssue && !connected 77 | ? 78 | : null} 79 |
80 |
81 | 85 | {loading 86 | ? 87 | : clonedChildren} 88 | 89 |
90 |
91 | ); 92 | } 93 | } 94 | 95 | App.propTypes = { 96 | user: React.PropTypes.object, // current meteor user 97 | connected: React.PropTypes.bool, // server connection status 98 | loading: React.PropTypes.bool, // subscription status 99 | menuOpen: React.PropTypes.bool, // is side menu open? 100 | lists: React.PropTypes.array, // all lists visible to the current user 101 | children: React.PropTypes.element, // matched child route component 102 | location: React.PropTypes.object // current router location 103 | }; 104 | 105 | App.contextTypes = { 106 | router: React.PropTypes.object 107 | }; 108 | -------------------------------------------------------------------------------- /imports/ui/layouts/App.less: -------------------------------------------------------------------------------- 1 | @import '{}/imports/ui/stylesheets/utils.less'; 2 | 3 | @menu-width: 270px; 4 | @column: 5.55555%; 5 | 6 | body { 7 | .position(absolute, 0, 0, 0, 0); 8 | background-color: #315481; 9 | background-image: linear-gradient(to bottom, #315481, #918e82 100%); 10 | background-repeat: no-repeat; 11 | background-attachment: fixed; 12 | } 13 | 14 | #container { 15 | .position(absolute, 0, 0, 0, 0); 16 | 17 | @media screen and (min-width: 60em) { 18 | left: @column; 19 | right: @column; 20 | } 21 | 22 | @media screen and (min-width: 80em) { 23 | left: 2*@column; 24 | right: 2*@column; 25 | } 26 | 27 | // Hide anything offscreen 28 | overflow: hidden; 29 | } 30 | 31 | #menu { 32 | .position(absolute, 0, 0, 0, 0, @menu-width); 33 | overflow-y: auto; 34 | -webkit-overflow-scrolling: touch; 35 | } 36 | 37 | #content-container { 38 | .position(absolute, 0, 0, 0, 0); 39 | transition: all 200ms ease-out; 40 | transform: translate3d(0, 0, 0); 41 | background: @color-tertiary; 42 | opacity: 1; 43 | 44 | @media screen and (min-width: 40em) { 45 | left: @menu-width; 46 | } 47 | 48 | .content-scrollable { 49 | .position(absolute, 0, 0, 0, 0); 50 | transform: translate3d(0, 0, 0); 51 | overflow-y: auto; 52 | -webkit-overflow-scrolling: touch; 53 | } 54 | 55 | // Toggle menu on mobile 56 | .menu-open & { 57 | transform: translate3d(@menu-width, 0, 0); 58 | opacity: .85; 59 | left: 0; 60 | 61 | @media screen and (min-width: 40em) { 62 | // Show menu on desktop, negate .menu-open 63 | transform: translate3d(0, 0, 0); //reset transform and use position properties instead 64 | opacity: 1; 65 | left: @menu-width; 66 | } 67 | } 68 | } 69 | 70 | // Transparent screen to prevent interactions on content when menu is open 71 | .content-overlay { 72 | .position(absolute, 0, 0, 0, 0); 73 | cursor: pointer; 74 | 75 | .menu-open & { 76 | transform: translate3d(@menu-width, 0, 0); 77 | z-index: 1; 78 | } 79 | 80 | // Hide overlay on desktop 81 | @media screen and (min-width: 40em) { display: none; } 82 | } 83 | -------------------------------------------------------------------------------- /imports/ui/pages/AuthPage.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import MobileMenu from '../components/MobileMenu.jsx'; 3 | 4 | // a common layout wrapper for auth pages 5 | const AuthPage = ({ content, link }) => ( 6 |
7 | 10 |
11 | {content} 12 | {link} 13 |
14 |
15 | ); 16 | 17 | AuthPage.propTypes = { 18 | content: React.PropTypes.element, 19 | link: React.PropTypes.element 20 | }; 21 | 22 | export default AuthPage; 23 | -------------------------------------------------------------------------------- /imports/ui/pages/AuthPage.less: -------------------------------------------------------------------------------- 1 | @import '{}/imports/ui/stylesheets/utils.less'; 2 | 3 | .page.auth { 4 | text-align: center; 5 | 6 | .content-scrollable { background: @color-tertiary; } 7 | 8 | .wrapper-auth { 9 | padding-top: 4em; 10 | 11 | @media screen and (min-width: 40em) { 12 | margin: 0 auto; 13 | max-width: 480px; 14 | width: 80%; 15 | } 16 | 17 | .title-auth { 18 | .font-l1; 19 | .type-light; 20 | color: @color-ancillary; 21 | margin-bottom: .75rem; 22 | } 23 | 24 | .subtitle-auth { 25 | color: @color-medium-well; 26 | margin: 0 15% 3rem; 27 | } 28 | 29 | form { 30 | .input-symbol { 31 | margin-bottom: 1px; 32 | width: 100%; 33 | } 34 | 35 | .btn-primary { 36 | margin: 1em 5% 0; 37 | width: 90%; 38 | 39 | @media screen and (min-width: 40em) { 40 | margin-left: 0; 41 | margin-right: 0; 42 | width: 100%; 43 | } 44 | } 45 | } 46 | .list-errors { 47 | margin-top: -2rem; 48 | .list-item { 49 | .title-caps; 50 | background: @color-note; 51 | color: @color-negative; 52 | font-size: .625em; // 10px 53 | margin-bottom: 1px; 54 | padding: .7rem 0; 55 | } 56 | } 57 | } 58 | 59 | .link-auth-alt { 60 | .font-s1; 61 | .position(absolute, auto, 0, 1em, 0); 62 | color: @color-medium; 63 | display: inline-block; 64 | 65 | @media screen and (min-width: 40em) { 66 | bottom: 0; 67 | margin-top: 1rem; 68 | position: relative; 69 | } 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /imports/ui/pages/AuthPageJoin.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import AuthPage from './AuthPage.jsx'; 3 | import { Link } from 'react-router'; 4 | import { Accounts } from 'meteor/accounts-base'; 5 | 6 | export default class JoinPage extends React.Component { 7 | constructor(props) { 8 | super(props); 9 | this.state = { errors: {}}; 10 | } 11 | 12 | onSubmit(event) { 13 | event.preventDefault(); 14 | const email = this.refs.email.value; 15 | const password = this.refs.password.value; 16 | const confirm = this.refs.confirm.value; 17 | const errors = {}; 18 | 19 | if (!email) { 20 | errors.email = 'Email required'; 21 | } 22 | if (!password) { 23 | errors.password = 'Password required'; 24 | } 25 | if (confirm !== password) { 26 | errors.confirm = 'Please confirm your password'; 27 | } 28 | 29 | this.setState({ errors }); 30 | if (Object.keys(errors).length) { 31 | return; 32 | } 33 | 34 | Accounts.createUser({ 35 | email, 36 | password 37 | }, err => { 38 | if (err) { 39 | this.setState({ 40 | errors: { 'none': err.reason } 41 | }); 42 | } 43 | this.context.router.push('/'); 44 | }); 45 | } 46 | 47 | render() { 48 | const { errors } = this.state; 49 | const errorMessages = Object.keys(errors).map(key => errors[key]); 50 | const errorClass = key => errors[key] && 'error'; 51 | 52 | const content = ( 53 |
54 |

Join.

55 |

Joining allows you to make private lists

56 |
57 |
58 | {errorMessages.map(msg => ( 59 |
{msg}
60 | ))} 61 |
62 |
63 | 64 | 65 |
66 |
67 | 68 | 69 |
70 |
71 | 72 | 73 |
74 | 75 |
76 |
77 | ); 78 | 79 | const link = Have an account? Sign in; 80 | 81 | return ; 82 | } 83 | } 84 | 85 | JoinPage.contextTypes = { 86 | router: React.PropTypes.object 87 | }; 88 | -------------------------------------------------------------------------------- /imports/ui/pages/AuthPageSignIn.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import AuthPage from './AuthPage.jsx'; 3 | import { Link } from 'react-router'; 4 | 5 | export default class SignInPage extends React.Component { 6 | constructor(props) { 7 | super(props); 8 | this.state = { errors: {}}; 9 | } 10 | 11 | onSubmit(event) { 12 | event.preventDefault(); 13 | const email = this.refs.email.value; 14 | const password = this.refs.password.value; 15 | const errors = {}; 16 | 17 | if (!email) { 18 | errors.email = 'Email required'; 19 | } 20 | if (!password) { 21 | errors.password = 'Password required'; 22 | } 23 | 24 | this.setState({ errors }); 25 | if (Object.keys(errors).length) { 26 | return; 27 | } 28 | 29 | Meteor.loginWithPassword(email, password, err => { 30 | if (err) { 31 | this.setState({ 32 | errors: { 'none': err.reason } 33 | }); 34 | } 35 | this.context.router.push('/'); 36 | }); 37 | } 38 | 39 | render() { 40 | const { errors } = this.state; 41 | const errorMessages = Object.keys(errors).map(key => errors[key]); 42 | const errorClass = key => errors[key] && 'error'; 43 | 44 | const content = ( 45 |
46 |

Sign In.

47 |

Signing in allows you to view private lists

48 |
49 |
50 | {errorMessages.map(msg => ( 51 |
{msg}
52 | ))} 53 |
54 |
55 | 56 | 57 |
58 |
59 | 60 | 61 |
62 | 63 |
64 |
65 | ); 66 | 67 | const link = Need an account? Join Now.; 68 | 69 | return ; 70 | } 71 | } 72 | 73 | SignInPage.contextTypes = { 74 | router: React.PropTypes.object 75 | }; 76 | -------------------------------------------------------------------------------- /imports/ui/pages/ListPage.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ListHeader from '../components/ListHeader.jsx'; 3 | import TodoItem from '../components/TodoItem.jsx'; 4 | import NotFoundPage from '../pages/NotFoundPage.jsx'; 5 | import Message from '../components/Message.jsx'; 6 | 7 | export default class ListPage extends React.Component { 8 | constructor(props) { 9 | super(props); 10 | this.state = { 11 | editingTodo: null 12 | }; 13 | } 14 | 15 | onEditingChange(id, editing) { 16 | this.setState({ 17 | editingTodo: editing ? id : null 18 | }); 19 | } 20 | 21 | render() { 22 | const { list, listExists, loading, todos } = this.props; 23 | const { editingTodo } = this.state; 24 | 25 | if (!listExists) { 26 | return ; 27 | } 28 | 29 | const Todos = !todos || !todos.length 30 | ? 33 | : todos.map(todo => ( 34 | 39 | )); 40 | 41 | return ( 42 |
43 | 44 |
45 | {loading ? : Todos} 46 |
47 |
48 | ); 49 | } 50 | } 51 | 52 | ListPage.propTypes = { 53 | list: React.PropTypes.object, 54 | todos: React.PropTypes.array, 55 | loading: React.PropTypes.bool, 56 | listExists: React.PropTypes.bool 57 | }; 58 | -------------------------------------------------------------------------------- /imports/ui/pages/ListPage.less: -------------------------------------------------------------------------------- 1 | @import '{}/imports/ui/stylesheets/utils.less'; 2 | 3 | .page.lists-show { 4 | .content-scrollable { 5 | background: @color-empty; 6 | top: 5em !important; 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /imports/ui/pages/NotFoundPage.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import MobileMenu from '../components/MobileMenu.jsx'; 3 | import Message from '../components/Message.jsx'; 4 | 5 | const NotFoundPage = () => ( 6 |
7 | 10 |
11 | 12 |
13 |
14 | ); 15 | 16 | export default NotFoundPage; 17 | -------------------------------------------------------------------------------- /imports/ui/pages/NotFoundPage.less: -------------------------------------------------------------------------------- 1 | @import '{}/imports/ui/stylesheets/utils.less'; 2 | 3 | .page.not-found { 4 | .content-scrollable { background: @color-tertiary; } 5 | } 6 | -------------------------------------------------------------------------------- /imports/ui/stylesheets/base.less: -------------------------------------------------------------------------------- 1 | @import './utils.less'; 2 | 3 | * { 4 | box-sizing: border-box; 5 | -webkit-tap-highlight-color:rgba(0,0,0,0); 6 | -webkit-tap-highlight-color: transparent; // for some Androids 7 | } 8 | 9 | html, button, input, textarea, select { 10 | outline: none; 11 | -webkit-font-smoothing: antialiased; 12 | -moz-osx-font-smoothing: grayscale; 13 | } 14 | 15 | body { 16 | .type-regular; 17 | color: @color-full; 18 | font-size: 16px; //this sets the baseline so we can use multiples of 4 & (r)ems 19 | } 20 | 21 | // Default type layout 22 | h1, h2, h3, h4, h5, h6 { 23 | .type-regular; 24 | margin: 0; 25 | padding: 0; 26 | } 27 | 28 | h1 { 29 | .font-l1; 30 | } 31 | 32 | h2 { 33 | .font-m3; 34 | } 35 | 36 | h3 { 37 | .font-m2; 38 | } 39 | 40 | h4 { 41 | .font-m1; 42 | } 43 | 44 | h5 { 45 | .font-s2; 46 | color: @color-medium-rare; 47 | text-transform: uppercase; 48 | } 49 | 50 | h6 { 51 | color: @color-medium; 52 | } 53 | 54 | p { 55 | .font-s3; 56 | } 57 | 58 | sub, 59 | sup { 60 | font-size: .8em; 61 | } 62 | 63 | sub { 64 | bottom: -.2em; 65 | } 66 | 67 | sup { 68 | top: -.2em; 69 | } 70 | 71 | b { 72 | font-weight: bold; 73 | } 74 | 75 | em { 76 | font-style: italic; 77 | } 78 | -------------------------------------------------------------------------------- /imports/ui/stylesheets/button.less: -------------------------------------------------------------------------------- 1 | @import './utils.less'; 2 | 3 | [class^="btn-"], 4 | [class*=" btn-"] { 5 | // Sizing 6 | .font-s2; 7 | line-height: 20px !important; //override line-height always so we can use em's to size 8 | padding: 1em 1.25em; // 48px tall 9 | 10 | // Style 11 | .title-caps; 12 | transition: all 200ms ease-in; 13 | color: @color-empty; 14 | display: inline-block; 15 | position: relative; 16 | text-align: center; 17 | text-decoration: none !important; //prevents global styles from applying 18 | vertical-align: middle; 19 | white-space: nowrap; 20 | 21 | &[class*="primary"] { 22 | background-color: @color-primary; 23 | color: @color-empty; 24 | 25 | &:hover { background-color: darken(@color-primary, 5%); } 26 | &:active { box-shadow: rgba(0,0,0,.3) 0 1px 3px 0 inset; } 27 | } 28 | 29 | &[class*="secondary"] { 30 | transition: all 300ms ease-in; 31 | box-shadow: lighten(#517096, 5%) 0 0 0 1px inset; 32 | color: @color-empty; 33 | 34 | &:hover{ color: @color-rare; } 35 | &:active, 36 | &.active { 37 | box-shadow: lighten(#517096, 25%) 0 0 0 1px inset; 38 | } 39 | } 40 | 41 | &[disabled] { opacity: .5; } 42 | } 43 | 44 | .btns-group { 45 | display: flex; 46 | flex-wrap: wrap; 47 | width: 100%; 48 | 49 | [class*="btn-"] { 50 | .ellipsized; 51 | flex: 1; 52 | 53 | & + [class*="btn-"] { margin-left: -1px; } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /imports/ui/stylesheets/fade-transition.less: -------------------------------------------------------------------------------- 1 | .fade-enter { 2 | opacity: 0.01; 3 | } 4 | 5 | .fade-enter.fade-enter-active { 6 | opacity: 1; 7 | transition: opacity 200ms ease-in; 8 | } 9 | 10 | .fade-leave { 11 | opacity: 1; 12 | } 13 | 14 | .fade-leave.fade-leave-active { 15 | opacity: 0.01; 16 | transition: opacity 200ms ease-in; 17 | } 18 | -------------------------------------------------------------------------------- /imports/ui/stylesheets/form.less: -------------------------------------------------------------------------------- 1 | @import './utils.less'; 2 | 3 | // Standard text input 4 | input[type="text"], 5 | input[type="email"], 6 | input[type="password"], 7 | textarea { 8 | // Sizing 9 | .font-s2; 10 | .type-regular; 11 | padding: .75rem 0; //total height ~48 12 | line-height: 1.5rem !important; 13 | 14 | // Style 15 | ::placeholder { 16 | color: @color-complementary; 17 | } 18 | 19 | border: none; 20 | border-radius: 0; 21 | box-sizing: border-box; 22 | color: @color-full; 23 | outline: none; 24 | 25 | &[disabled] { opacity: .5; } 26 | } 27 | 28 | // Remove chrome/saf autofill yellow background 29 | input:-webkit-autofill { 30 | -webkit-box-shadow: 0 0 0 1000px @color-empty inset; 31 | } 32 | 33 | // Custom checkbox 34 | .checkbox { 35 | display: inline-block; 36 | height: 3rem; 37 | position: relative; 38 | vertical-align: middle; 39 | width: 44px; 40 | 41 | input[type="checkbox"] { 42 | font-size: 1em; 43 | visibility: hidden; 44 | 45 | & + span:before { 46 | .position(absolute, 50%, auto, auto, 50%, .85em, .85em); 47 | transform: translate3d(-50%, -50%, 0); 48 | background: transparent; 49 | box-shadow: #abdfe3 0 0 0 1px inset; 50 | content: ''; 51 | display: block; 52 | } 53 | 54 | &:checked + span:before { 55 | box-shadow: none; 56 | color: @color-medium-rare; 57 | 58 | // Icon family from icon.lessimport 59 | font-family: 'todos'; 60 | speak: none; 61 | font-style: normal; 62 | font-weight: normal; 63 | font-variant: normal; 64 | text-transform: none; 65 | line-height: 1; 66 | 67 | // Better Font Rendering 68 | -webkit-font-smoothing: antialiased; 69 | -moz-osx-font-smoothing: grayscale; 70 | 71 | // Checkmark icon 72 | content: "\e612"; 73 | } 74 | } 75 | } 76 | 77 | // Input with an icon 78 | .input-symbol { 79 | display: inline-block; 80 | position: relative; 81 | 82 | &.error [class^="icon-"], 83 | &.error [class*=" icon-"] { 84 | color: @color-negative; 85 | } 86 | 87 | // Position & padding 88 | [class^="icon-"], 89 | [class*=" icon-"] { 90 | left: 1em; 91 | } 92 | 93 | input { padding-left: 3em; } 94 | 95 | // Styling 96 | input { 97 | width: 100%; 98 | 99 | &:focus { 100 | & + [class^="icon-"], 101 | & + [class*=" icon-"] { 102 | color: @color-primary; 103 | } 104 | } 105 | } 106 | 107 | [class^="icon-"], 108 | [class*=" icon-"] { 109 | transition: all 300ms ease-in; 110 | transform: translate3d(0,-50%,0); 111 | background: transparent; 112 | color: @color-medium; 113 | font-size: 1em; 114 | height: 1em; 115 | position: absolute; 116 | top: 50%; 117 | width: 1em; 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /imports/ui/stylesheets/icon.less: -------------------------------------------------------------------------------- 1 | @import './utils.less'; 2 | 3 | @font-face { 4 | font-family: 'todos'; 5 | src:url('/icon/todos.eot?-5w3um4'); 6 | src:url('/icon/todos.eot?#iefix5w3um4') format('embedded-opentype'), 7 | url('/icon/todos.woff?5w3um4') format('woff'), 8 | url('/icon/todos.ttf?5w3um4') format('truetype'), 9 | url('/icon/todos.svg?5w3um4#todos') format('svg'); 10 | font-weight: normal; 11 | font-style: normal; 12 | } 13 | 14 | [class^="icon-"], [class*=" icon-"] { 15 | font-family: 'todos'; 16 | speak: none; 17 | font-style: normal; 18 | font-weight: normal; 19 | font-variant: normal; 20 | text-transform: none; 21 | line-height: 1; 22 | 23 | // Better Font Rendering 24 | -webkit-font-smoothing: antialiased; 25 | -moz-osx-font-smoothing: grayscale; 26 | } 27 | 28 | 29 | .icon-unlock:before { 30 | content: "\e600"; 31 | } 32 | .icon-user-add:before { 33 | content: "\e604"; 34 | } 35 | .icon-cog:before { 36 | content: "\e606"; 37 | } 38 | .icon-trash:before { 39 | content: "\e607"; 40 | } 41 | .icon-edit:before { 42 | content: "\e608"; 43 | } 44 | .icon-add:before { 45 | content: "\e60a"; 46 | } 47 | .icon-plus:before { 48 | content: "\e60b"; 49 | } 50 | .icon-close:before { 51 | content: "\e60c"; 52 | } 53 | .icon-cross:before { 54 | content: "\e60d"; 55 | } 56 | .icon-sync:before { 57 | content: "\e60e"; 58 | } 59 | .icon-lock:before { 60 | content: "\e610"; 61 | } 62 | .icon-check:before { 63 | content: "\e612"; 64 | } 65 | .icon-share:before { 66 | content: "\e617"; 67 | } 68 | .icon-email:before { 69 | content: "\e619"; 70 | } 71 | .icon-arrow-up:before { 72 | content: "\e623"; 73 | } 74 | .icon-arrow-down:before { 75 | content: "\e626"; 76 | } 77 | .icon-list-unordered:before { 78 | content: "\e634"; 79 | } 80 | -------------------------------------------------------------------------------- /imports/ui/stylesheets/link.less: -------------------------------------------------------------------------------- 1 | @import './utils.less'; 2 | 3 | a { 4 | transition: all 200ms ease-in; 5 | color: @color-secondary; 6 | cursor: pointer; 7 | text-decoration: none; 8 | 9 | &:hover { color: darken(@color-primary, 10%); } 10 | &:active { color: @color-well; } 11 | &:focus { outline:none; } //removes FF dotted outline 12 | } 13 | -------------------------------------------------------------------------------- /imports/ui/stylesheets/nav.less: -------------------------------------------------------------------------------- 1 | @import './utils.less'; 2 | 3 | // Generic nav positioning and styles 4 | nav { 5 | .position(absolute, 0, 0, auto, 0); 6 | transform: translate3d(0,0,0); 7 | transition: all 200ms ease-out; 8 | z-index: 10; 9 | 10 | .nav-item { 11 | .font-m1; 12 | color: @color-ancillary; 13 | display: inline-block; 14 | height: 3rem; 15 | text-align: center; 16 | width: 3rem; 17 | 18 | &:active { opacity: .5; } 19 | 20 | [class^="icon-"], 21 | [class*=" icon-"] { 22 | line-height: 3rem; 23 | vertical-align: middle; 24 | } 25 | } 26 | .nav-group { 27 | .position(absolute, 0, auto, auto, 0); 28 | z-index: 1; 29 | 30 | &.right { 31 | left: auto; 32 | right: 0; 33 | } 34 | } 35 | } 36 | 37 | // Custom nav for auth 38 | @media screen and (min-width: 40em) { 39 | .page.auth .nav-group { display: none; } 40 | .page.not-found .nav-group { display: none; } 41 | } 42 | -------------------------------------------------------------------------------- /imports/ui/stylesheets/reset.less: -------------------------------------------------------------------------------- 1 | /* Reset.less 2 | * Props to Eric Meyer (meyerweb.com) for his CSS reset file. We're using an adapted version here that cuts out some of the reset HTML elements we will never need here (i.e., dfn, samp, etc). 3 | * ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- */ 4 | 5 | 6 | // ERIC MEYER RESET 7 | // -------------------------------------------------- 8 | 9 | html, body { margin: 0; padding: 0; } 10 | h1, h2, h3, h4, h5, h6, p, blockquote, pre, a, abbr, acronym, address, cite, code, del, dfn, em, img, q, s, samp, small, strike, strong, sub, sup, tt, var, dd, dl, dt, li, ol, ul, fieldset, form, label, legend, button, table, caption, tbody, tfoot, thead, tr, th, td { margin: 0; padding: 0; border: 0; font-weight: normal; font-style: normal; font-size: 100%; line-height: 1; font-family: inherit; } 11 | table { border-collapse: collapse; border-spacing: 0; } 12 | ol, ul { list-style: none; } 13 | q:before, q:after, blockquote:before, blockquote:after { content: ""; } 14 | 15 | 16 | // Normalize.css 17 | // Pulling in select resets form the normalize.css project 18 | // -------------------------------------------------- 19 | 20 | // Display in IE6-9 and FF3 21 | // ------------------------- 22 | // Source: http://github.com/necolas/normalize.css 23 | html { 24 | font-size: 100%; 25 | -webkit-text-size-adjust: 100%; 26 | -ms-text-size-adjust: 100%; 27 | } 28 | // Focus states 29 | a:focus { 30 | outline: thin dotted; 31 | } 32 | // Hover & Active 33 | a:hover, 34 | a:active { 35 | outline: 0; 36 | } 37 | 38 | // Display in IE6-9 and FF3 39 | // ------------------------- 40 | // Source: http://github.com/necolas/normalize.css 41 | article, 42 | aside, 43 | details, 44 | figcaption, 45 | figure, 46 | footer, 47 | header, 48 | hgroup, 49 | nav, 50 | section { 51 | display: block; 52 | } 53 | 54 | // Display block in IE6-9 and FF3 55 | // ------------------------- 56 | // Source: http://github.com/necolas/normalize.css 57 | audio, 58 | canvas, 59 | video { 60 | display: inline-block; 61 | *display: inline; 62 | *zoom: 1; 63 | } 64 | 65 | // Prevents modern browsers from displaying 'audio' without controls 66 | // ------------------------- 67 | // Source: http://github.com/necolas/normalize.css 68 | audio:not([controls]) { 69 | display: none; 70 | } 71 | 72 | // Prevents sub and sup affecting line-height in all browsers 73 | // ------------------------- 74 | // Source: http://github.com/necolas/normalize.css 75 | sub, 76 | sup { 77 | font-size: 75%; 78 | line-height: 0; 79 | position: relative; 80 | vertical-align: baseline; 81 | } 82 | sup { 83 | top: -0.5em; 84 | } 85 | sub { 86 | bottom: -0.25em; 87 | } 88 | 89 | // Img border in a's and image quality 90 | // ------------------------- 91 | // Source: http://github.com/necolas/normalize.css 92 | img { 93 | border: 0; 94 | -ms-interpolation-mode: bicubic; 95 | } 96 | 97 | // Forms 98 | // ------------------------- 99 | // Source: http://github.com/necolas/normalize.css 100 | 101 | // Font size in all browsers, margin changes, misc consistency 102 | button, 103 | input, 104 | select, 105 | textarea { 106 | font-size: 100%; 107 | margin: 0; 108 | vertical-align: baseline; 109 | *vertical-align: middle; 110 | } 111 | button, 112 | input { 113 | line-height: normal; // FF3/4 have !important on line-height in UA stylesheet 114 | *overflow: visible; // Inner spacing ie IE6/7 115 | } 116 | button::-moz-focus-inner, 117 | input::-moz-focus-inner { // Inner padding and border oddities in FF3/4 118 | border: 0; 119 | padding: 0; 120 | } 121 | button, 122 | input[type="button"], 123 | input[type="reset"], 124 | input[type="submit"] { 125 | cursor: pointer; // Cursors on all buttons applied consistently 126 | -webkit-appearance: button; // Style clicable inputs in iOS 127 | } 128 | input[type="search"] { // Appearance in Safari/Chrome 129 | -webkit-appearance: textfield; 130 | -webkit-box-sizing: content-box; 131 | -moz-box-sizing: content-box; 132 | box-sizing: content-box; 133 | } 134 | input[type="search"]::-webkit-search-decoration { 135 | -webkit-appearance: none; // Inner-padding issues in Chrome OSX, Safari 5 136 | } 137 | textarea { 138 | overflow: auto; // Remove vertical scrollbar in IE6-9 139 | vertical-align: top; // Readability and alignment cross-browser 140 | } -------------------------------------------------------------------------------- /imports/ui/stylesheets/util/fontface.less: -------------------------------------------------------------------------------- 1 | // Light 2 | @font-face { 3 | font-family: 'Open Sans'; 4 | src: url('/font/OpenSans-Light-webfont.eot'); 5 | src: url('/font/OpenSans-Light-webfont.eot?#iefix') format('embedded-opentype'), 6 | url('/font/OpenSans-Light-webfont.woff') format('woff'), 7 | url('/font/OpenSans-Light-webfont.ttf') format('truetype'), 8 | url('/font/OpenSans-Light-webfont.svg#OpenSansLight') format('svg'); 9 | font-weight: 200; 10 | font-style: normal; 11 | } 12 | 13 | // Regular 14 | @font-face { 15 | font-family: 'Open Sans'; 16 | src: url('/font/OpenSans-Regular-webfont.eot'); 17 | src: url('/font/OpenSans-Regular-webfont.eot?#iefix') format('embedded-opentype'), 18 | url('/font/OpenSans-Regular-webfont.woff') format('woff'), 19 | url('/font/OpenSans-Regular-webfont.ttf') format('truetype'), 20 | url('/font/OpenSans-Regular-webfont.svg#OpenSansRegular') format('svg'); 21 | font-weight: normal; 22 | font-weight: 400; 23 | font-style: normal; 24 | } 25 | -------------------------------------------------------------------------------- /imports/ui/stylesheets/util/helpers.less: -------------------------------------------------------------------------------- 1 | .position(@type; @top: auto; @right: auto; @bottom: auto; @left: auto; @width: auto; @height: auto) { 2 | position: @type; 3 | top: @top; 4 | right: @right; 5 | bottom: @bottom; 6 | left: @left; 7 | width: @width; 8 | height: @height; 9 | } -------------------------------------------------------------------------------- /imports/ui/stylesheets/util/text.less: -------------------------------------------------------------------------------- 1 | // Caps styling used in headers 2 | .title-caps() { 3 | letter-spacing: .3em; 4 | text-indent: .3em; 5 | text-transform: uppercase; 6 | } 7 | 8 | // Adds an ellipses at the end of overflowing strings 9 | .ellipsized() { 10 | overflow: hidden; 11 | text-overflow: ellipsis; 12 | white-space: nowrap; 13 | } 14 | 15 | .force-wrap { 16 | word-wrap: break-word; 17 | word-break: break-all; 18 | -ms-word-break: break-all; 19 | word-break: break-word; // Non-standard for webkit 20 | hyphens: auto; 21 | } 22 | -------------------------------------------------------------------------------- /imports/ui/stylesheets/util/typography.less: -------------------------------------------------------------------------------- 1 | .type-regular() { 2 | font-family: 'Open Sans', "Helvetica Neue", Helvetica, Arial, sans-serif; 3 | font-style: 400; 4 | } 5 | 6 | .type-light { 7 | font-family: 'Open Sans', "Helvetica Neue", Helvetica, Arial, sans-serif; 8 | font-weight: 300; 9 | } 10 | 11 | 12 | // Large fonts 13 | .font-l3() { 14 | font-size: 56px; 15 | line-height: 64px; 16 | } 17 | 18 | .font-l2() { 19 | font-size: 48px; 20 | line-height: 56px; 21 | } 22 | 23 | .font-l1() { 24 | font-size: 40px; 25 | line-height: 48px; 26 | } 27 | 28 | // Medium fonts 29 | .font-m3() { 30 | font-size: 28px; 31 | line-height: 32px; 32 | } 33 | 34 | .font-m2() { 35 | font-size: 24px; 36 | line-height: 28px; 37 | } 38 | 39 | .font-m1() { 40 | font-size: 20px; 41 | line-height: 24px; 42 | } 43 | 44 | // Small fonts 45 | .font-s3() { 46 | font-size: 16px; 47 | line-height: 24px; 48 | } 49 | 50 | .font-s2() { 51 | font-size: 14px; 52 | line-height: 20px; 53 | } 54 | 55 | .font-s1() { 56 | font-size: 12px; 57 | line-height: 16px; 58 | } -------------------------------------------------------------------------------- /imports/ui/stylesheets/util/variables.less: -------------------------------------------------------------------------------- 1 | // Core 2 | @color-primary: #2cc5d2; //caribbean teal (buttons) 3 | @color-secondary: #5db9ff; //cerulean blue (menu new list) 4 | @color-tertiary: #d2edf4; //muted teal (join/signin bg) 5 | @color-ancillary: #1c3f53; //deep navy (nav heading, menu icon) 6 | @color-complementary: #778b91; //muted navy (add item placeholder) 7 | 8 | // Alert 9 | @color-negative: #ff4400; //error, alert 10 | @color-note: #f6fccf; 11 | 12 | // Greyscale 13 | @color-empty: white; 14 | @color-raw: #f8f8f8; 15 | @color-raw: #f2f2f2; 16 | @color-rare: #eee; 17 | @color-medium-rare: #ccc; 18 | @color-medium: #aaa; 19 | @color-medium-well: #666; 20 | @color-well: #555; 21 | @color-full: #333; -------------------------------------------------------------------------------- /imports/ui/stylesheets/utils.less: -------------------------------------------------------------------------------- 1 | @import 'util/helpers.less'; 2 | @import 'util/fontface.less'; 3 | @import 'util/text.less'; 4 | @import 'util/typography.less'; 5 | @import 'util/variables.less'; 6 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "module-simple-todos-react", 3 | "version": "1.0.0", 4 | "description": "todos", 5 | "scripts": { 6 | "lint": "eslint --ext js,jsx ." 7 | }, 8 | "dependencies": { 9 | "classnames": "^2.2.3", 10 | "react": "^0.14.7", 11 | "react-addons-css-transition-group": "^0.14.7", 12 | "react-addons-pure-render-mixin": "^0.14.7", 13 | "react-dom": "^0.14.7", 14 | "react-komposer": "^1.3.0", 15 | "react-router": "^2.0.0" 16 | }, 17 | "author": "Evan You", 18 | "license": "MIT", 19 | "devDependencies": { 20 | "babel-eslint": "^4.1.8", 21 | "eslint": "^1.10.3", 22 | "eslint-plugin-react": "^3.16.1" 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /packages/factory/README.md: -------------------------------------------------------------------------------- 1 | # Factory 2 | 3 | A factory package for Meteor. 4 | 5 | This version is very similar to https://atmospherejs.com/dburles/factory although internally it's implementation is quite different. 6 | 7 | ## Usage 8 | 9 | To define a factory: 10 | 11 | ``` 12 | Factory.define("app", Apps, { 13 | name: function() { return Faker.name(); } 14 | }); 15 | ``` 16 | 17 | To create an app and insert it into the collection 18 | (usually for things like unit tests): 19 | ``` 20 | Factory.create("app"); 21 | ``` 22 | 23 | 24 | To build an app, appropriate for `Apps.insert` (or other purposes, e.g. styleguide): 25 | ``` 26 | Factory.build("app") 27 | ``` 28 | 29 | ### Relationships 30 | 31 | A key thing is relationships between factories. 32 | 33 | ``` 34 | Factory.define("appVersion", AppVersions, { 35 | app: Factory.get("app") 36 | }); 37 | ``` 38 | 39 | If we call `Factory.create("appVersion")`, it will return a single version, but *will also insert an app into the `Apps`*. The `app` key on the version will allow you to find it. 40 | 41 | If you call `Factory.build("appVersion")` *no app* will be created and the `app` key will be set to a random id (NOTE: this may be a bad idea and quite confusing). 42 | 43 | There is a more useful method for this case: 44 | 45 | ``` 46 | Factory.compile("appVersion") 47 | ``` 48 | 49 | This returns an object with *collection names* as keys and an array of build documents as values, for instance: 50 | ``` 51 | { 52 | appVersions: [{...}], 53 | apps: [{...}] 54 | } 55 | ``` 56 | 57 | This is useful for styleguide specs for instance, as the component will usually need some or all of this data. 58 | 59 | ## TODO 60 | 61 | - JSdocs 62 | - Support mutators/validation for insert 63 | - afterCreate / decide on what after does 64 | - make extend a little saner? 65 | - decide if we'll send a PR to dburles or call this a separate package. 66 | - Allow "runtime" dependencies between packages 67 | - Seed approach for unit test consistency and speed. 68 | -------------------------------------------------------------------------------- /packages/factory/dataset.js: -------------------------------------------------------------------------------- 1 | /* global Factory */ 2 | 3 | function Dataset() { 4 | this.documents = {}; 5 | this.collections = {}; 6 | } 7 | 8 | _.extend(Dataset.prototype, { 9 | add(nameOrFactory, properties, opts) { 10 | const options = opts || {}; 11 | 12 | let factory; 13 | if (_.isString(nameOrFactory)) { 14 | factory = Factory.get(nameOrFactory); 15 | } else { 16 | factory = nameOrFactory; 17 | } 18 | 19 | let doc = factory.build(this, properties, _.pick(options || {}, 'noRelations')); 20 | if (options && options.target) { 21 | // We need to apply the transform from the collection as we aren't inserting anywhere 22 | if (factory.collection._transform) { 23 | doc = factory.collection._transform(doc); 24 | } 25 | 26 | this.targetDocId = doc._id; 27 | this.targetDocCollection = factory.collection; 28 | } 29 | return doc; 30 | }, 31 | 32 | addDocument(document, collection) { 33 | const collectionName = collection._name; 34 | 35 | if (!this.documents[collectionName]) { 36 | this.documents[collectionName] = []; 37 | this.collections[collectionName] = collection; 38 | } 39 | 40 | this.documents[collectionName].push(document); 41 | }, 42 | 43 | createAll() { 44 | const self = this; 45 | 46 | _.each(self.documents, function(docs, collectionName) { 47 | _.each(docs, function(doc) { 48 | self.collections[collectionName].insert(doc); 49 | }); 50 | }); 51 | }, 52 | 53 | get(collectionName, id) { 54 | // XXX: this could be a lot more efficient if we used a collection from the beginning 55 | const doc = _.find(this.documents[collectionName], function(d) { return d._id === id; }); 56 | const transform = this.collections[collectionName]._transform; 57 | if (transform) { 58 | return transform(doc); 59 | } 60 | return doc; 61 | }, 62 | 63 | getTargetDoc() { 64 | return this.get(this.targetDocCollection._name, this.targetDocId); 65 | }, 66 | 67 | getAsCollection(collectionName) { 68 | // NOTE: this should be something more featured like StubCollections.stubCollection 69 | // as it should clone the schema etc also. Maybe it doesn't matter... 70 | const collection = new Mongo.Collection(null, { 71 | transform: this.collections[collectionName]._transform 72 | }); 73 | 74 | _.each(this.documents[collectionName], function(doc) { 75 | collection.insert(doc); 76 | }); 77 | 78 | return collection; 79 | }, 80 | 81 | getAsCursor(collectionName) { 82 | const collection = this.getAsCollection(collectionName); 83 | return collection.find(); 84 | } 85 | }); 86 | 87 | Factory.Dataset = Dataset; 88 | -------------------------------------------------------------------------------- /packages/factory/factory-api.js: -------------------------------------------------------------------------------- 1 | /* global Factory */ 2 | 3 | Factory._factories = {}; 4 | 5 | Factory.get = function(name) { 6 | const factory = Factory._factories[name]; 7 | if (!factory) { 8 | throw new Meteor.Error(`No factory defined named ${name}`); 9 | } 10 | return factory; 11 | }; 12 | 13 | Factory.define = function(name, collection, properties) { 14 | Factory._factories[name] = new Factory(name, collection, properties); 15 | return Factory.get(name); 16 | }; 17 | 18 | 19 | Factory.create = function(name, properties) { 20 | const dataset = Factory.compile(name, properties); 21 | dataset.createAll(); 22 | return dataset.targetDocCollection.findOne(dataset.targetDocId); 23 | }; 24 | 25 | Factory.compile = function(name, properties, options) { 26 | const dataset = new Factory.Dataset(); 27 | const factory = Factory.get(name); 28 | dataset.add(factory, properties, _.extend({target: true}, options)); 29 | return dataset; 30 | }; 31 | 32 | Factory.build = function(name, properties) { 33 | const dataset = Factory.compile(name, properties, {noRelations: true}); 34 | return dataset.getTargetDoc(); 35 | }; 36 | 37 | Factory.extend = function(name, properties) { 38 | return _.extend({}, Factory.get(name).properties, properties || {}); 39 | }; 40 | -------------------------------------------------------------------------------- /packages/factory/factory-tests.js: -------------------------------------------------------------------------------- 1 | /* global Authors:true, Books:true */ 2 | /* global Factory */ 3 | 4 | Authors = new Meteor.Collection('authors-factory'); 5 | Books = new Meteor.Collection('books-factory'); 6 | 7 | Tinytest.add('Factory - Build - Basic build works', function(test) { 8 | Factory.define('author', Authors, { 9 | name: 'John Smith' 10 | }); 11 | 12 | test.equal(Factory.build('author').name, 'John Smith'); 13 | }); 14 | 15 | Tinytest.add('Factory - Build - Basic build lets you set _id', function(test) { 16 | Factory.define('author', Authors, { 17 | _id: 'my-id' 18 | }); 19 | 20 | test.equal(Factory.build('author')._id, 'my-id'); 21 | }); 22 | 23 | 24 | Tinytest.add('Factory - Define - AfterBuild hook', function(test) { 25 | let result; 26 | 27 | Factory.define('author', Authors, { 28 | name: 'John Smith' 29 | }).afterBuild(function(doc) { 30 | result = doc; 31 | }); 32 | 33 | const author = Factory.create('author'); 34 | test.equal(author.name, 'John Smith'); 35 | test.equal(result.name, 'John Smith'); 36 | }); 37 | 38 | 39 | Tinytest.add('Factory - Compile - AfterBuild hook that builds', function(test) { 40 | Factory.define('authorWithFriends', Authors, { 41 | name: 'John Smith' 42 | }).afterBuild(function(doc, set) { 43 | doc.friendIds = _.times(2, function() { 44 | return set.add('author')._id; 45 | }); 46 | }); 47 | 48 | const dataset = Factory.compile('authorWithFriends'); 49 | const author = dataset.getTargetDoc(); 50 | test.equal(author.friendIds.length, 2); 51 | test.equal(dataset.documents.authors.length, 3); 52 | }); 53 | 54 | Tinytest.add('Factory - Create - After hook that builds', function(test) { 55 | Factory.define('authorWithFriends', Authors, { 56 | name: 'John Smith' 57 | }).afterBuild(function(doc, dataset) { 58 | doc.friendIds = _.times(2, function() { 59 | return dataset.add('author')._id; 60 | }); 61 | }); 62 | 63 | const author = Factory.create('authorWithFriends'); 64 | test.equal(author.friendIds.length, 2); 65 | test.isTrue(!!Authors.findOne(author.friendIds[0])); 66 | }); 67 | 68 | Tinytest.add('Factory - Build - Functions - Basic', function(test) { 69 | Factory.define('author', Authors, { 70 | name: function() { 71 | return 'John Smith'; 72 | } 73 | }); 74 | 75 | test.equal(Factory.build('author').name, 'John Smith'); 76 | }); 77 | 78 | Tinytest.add('Factory - Build - Functions - Context', function(test) { 79 | Factory.define('author', Authors, { 80 | test: 'John Smith', 81 | name: function() { 82 | return this.test; 83 | } 84 | }); 85 | 86 | test.equal(Factory.build('author').name, 'John Smith'); 87 | }); 88 | 89 | Tinytest.add('Factory - Build - Dotted properties - Basic', function(test) { 90 | Factory.define('author', Authors, { 91 | 'profile.name': 'John Smith' 92 | }); 93 | 94 | test.equal(Factory.build('author').profile.name, 'John Smith'); 95 | }); 96 | 97 | Tinytest.add('Factory - Build - Dotted properties - Context', function(test) { 98 | Factory.define('author', Authors, { 99 | name: 'John Smith', 100 | 'profile.name': function() { 101 | return this.name; 102 | } 103 | }); 104 | 105 | test.equal(Factory.build('author').profile.name, 'John Smith'); 106 | }); 107 | 108 | Tinytest.add('Factory - Build - Deep objects', function(test) { 109 | Factory.define('author', Authors, { 110 | profile: { 111 | name: 'John Smith' 112 | } 113 | }); 114 | 115 | test.equal(Factory.build('author').profile.name, 'John Smith'); 116 | }); 117 | 118 | Tinytest.add('Factory - Build - Functions - Deep object - Basic', function(test) { 119 | Factory.define('author', Authors, { 120 | profile: { 121 | name: function() { 122 | return 'John Smith'; 123 | } 124 | } 125 | }); 126 | 127 | test.equal(Factory.build('author').profile.name, 'John Smith'); 128 | }); 129 | 130 | Tinytest.add('Factory - Build - Functions - Deep object - Context', function(test) { 131 | Factory.define('author', Authors, { 132 | name: 'John Smith', 133 | profile: { 134 | name: function() { 135 | return this.name; 136 | } 137 | } 138 | }); 139 | 140 | test.equal(Factory.build('author').profile.name, 'John Smith'); 141 | }); 142 | 143 | Tinytest.add('Factory - Build - Extend - Basic', function(test) { 144 | Factory.define('author', Authors, { 145 | name: 'John Smith' 146 | }); 147 | 148 | Factory.define('authorOne', Authors, Factory.extend('author')); 149 | 150 | test.equal(Factory.build('authorOne').name, 'John Smith'); 151 | }); 152 | 153 | Tinytest.add('Factory - Build - Extend - With attributes', function(test) { 154 | Factory.define('author', Authors, { 155 | name: 'John Smith' 156 | }); 157 | 158 | Factory.define('authorOne', Authors, Factory.extend('author', { 159 | test: 'testing!' 160 | })); 161 | 162 | test.equal(Factory.build('authorOne').name, 'John Smith'); 163 | test.equal(Factory.build('authorOne').test, 'testing!'); 164 | }); 165 | 166 | Tinytest.add('Factory - Build - Extend - With attributes (check that we do not modify the parent)', 167 | function(test) { 168 | Factory.define('author', Authors, { 169 | name: 'John Smith' 170 | }); 171 | 172 | Factory.define('authorOne', Books, Factory.extend('author', { 173 | test: 'testing!' 174 | })); 175 | 176 | const authorOne = Factory.build('authorOne'); 177 | const author = Factory.build('author'); 178 | 179 | test.equal(authorOne.name, 'John Smith'); 180 | test.equal(authorOne.test, 'testing!'); 181 | test.equal(_.isUndefined(author.test), true); 182 | } 183 | ); 184 | 185 | Tinytest.add('Factory - Build - Extend - Parent with relationship', function(test) { 186 | Factory.define('author', Authors, { 187 | name: 'John Smith' 188 | }); 189 | 190 | Factory.define('book', Books, { 191 | authorId: Factory.get('author'), 192 | name: 'A book', 193 | year: 2014 194 | }); 195 | 196 | Factory.define('bookOne', Books, Factory.extend('book')); 197 | 198 | const bookOne = Factory.create('bookOne'); 199 | 200 | test.equal(bookOne.name, 'A book'); 201 | }); 202 | 203 | Tinytest.add('Factory - Build - Extend - Parent with relationship - Extra attributes', 204 | function(test) { 205 | Factory.define('author', Authors, { 206 | name: 'John Smith' 207 | }); 208 | 209 | Factory.define('book', Books, { 210 | authorId: Factory.get('author'), 211 | name: 'A book', 212 | year: 2014 213 | }); 214 | 215 | Factory.define('bookOne', Books, Factory.extend('book', { 216 | name: 'A better book' 217 | })); 218 | 219 | const bookOne = Factory.create('bookOne'); 220 | 221 | test.equal(bookOne.name, 'A better book'); 222 | // same year as parent 223 | test.equal(bookOne.year, 2014); 224 | } 225 | ); 226 | 227 | Tinytest.add('Factory - Create - Basic', function(test) { 228 | Factory.define('author', Authors, { 229 | name: 'John Smith' 230 | }); 231 | 232 | const author = Factory.create('author'); 233 | 234 | test.equal(author.name, 'John Smith'); 235 | }); 236 | 237 | Tinytest.add('Factory - Create - Relationship', function(test) { 238 | Factory.define('author', Authors, { 239 | name: 'John Smith' 240 | }); 241 | 242 | Factory.define('book', Books, { 243 | authorId: Factory.get('author'), 244 | name: 'A book', 245 | year: 2014 246 | }); 247 | 248 | Authors.remove({}); 249 | const book = Factory.create('book'); 250 | 251 | test.equal(Authors.findOne(book.authorId).name, 'John Smith'); 252 | }); 253 | 254 | Tinytest.add('Factory - Create - Relationship - return a Factory from function', function(test) { 255 | Factory.define('author', Authors, { 256 | name: 'John Smith' 257 | }); 258 | 259 | Factory.define('book', Books, { 260 | authorId: function() { 261 | return Factory.get('author'); 262 | }, 263 | name: 'A book', 264 | year: 2014 265 | }); 266 | 267 | const book = Factory.create('book'); 268 | 269 | test.equal(Authors.findOne(book.authorId).name, 'John Smith'); 270 | }); 271 | 272 | Tinytest.add('Factory - Create - Relationship - return a Factory from deep function (dotted)', 273 | function(test) { 274 | Factory.define('author', Authors, { 275 | name: 'John Smith' 276 | }); 277 | 278 | Factory.define('book', Books, { 279 | 'good.authorId': function() { 280 | return Factory.get('author'); 281 | }, 282 | name: 'A book', 283 | year: 2014 284 | }); 285 | 286 | const book = Factory.create('book'); 287 | 288 | test.equal(Authors.findOne(book.good.authorId).name, 'John Smith'); 289 | } 290 | ); 291 | 292 | Tinytest.add('Factory - Create - Relationship - return a Factory from deep function', 293 | function(test) { 294 | Factory.define('author', Authors, { 295 | name: 'John Smith' 296 | }); 297 | 298 | Factory.define('book', Books, { 299 | good: { 300 | authorId: function() { 301 | return Factory.get('author'); 302 | } 303 | }, 304 | name: 'A book', 305 | year: 2014 306 | }); 307 | 308 | const book = Factory.create('book'); 309 | 310 | test.equal(Authors.findOne(book.good.authorId).name, 'John Smith'); 311 | } 312 | ); 313 | 314 | // TODO -- not yet implemented 315 | // Tinytest.add('Factory - Build - Sequence', function(test) { 316 | // Factory.define('author', Authors, { 317 | // name: 'John Smith', 318 | // email: function(factory) { 319 | // return factory.sequence(function(n) { 320 | // return 'person' + n + '@example.com'; 321 | // }); 322 | // } 323 | // }); 324 | // 325 | // const author = Factory.build('author'); 326 | // test.equal(author.email, 'person1@example.com'); 327 | // const author2 = Factory.build('author'); 328 | // test.equal(author2.email, 'person2@example.com'); 329 | // }); 330 | 331 | // Tinytest.add('Factory - Create - Sequence', function(test) { 332 | // Authors.remove({}); 333 | // 334 | // Factory.define('author', Authors, { 335 | // name: 'John Smith', 336 | // email: function(factory) { 337 | // return factory.sequence(function(n) { 338 | // return 'person' + n + '@example.com'; 339 | // }); 340 | // } 341 | // }); 342 | // 343 | // const author = Factory.create('author'); 344 | // test.equal(author.email, 'person1@example.com'); 345 | // const foundAuthor = Authors.find({email: 'person1@example.com'}).count(); 346 | // test.equal(foundAuthor, 1); 347 | // 348 | // const author2 = Factory.create('author'); 349 | // test.equal(author2.email, 'person2@example.com'); 350 | // const foundAuthor2 = Authors.find({email: 'person2@example.com'}).count(); 351 | // test.equal(foundAuthor2, 1); 352 | // }); 353 | -------------------------------------------------------------------------------- /packages/factory/factory.js: -------------------------------------------------------------------------------- 1 | /* global Factory:true */ 2 | /* global LocalCollection */ 3 | 4 | Factory = function(name, collection, properties) { 5 | this.name = name; 6 | this.collection = collection; 7 | this.properties = properties; 8 | this.afterBuildCbs = []; 9 | }; 10 | 11 | Factory.prototype.afterBuild = function(cb) { 12 | this.afterBuildCbs.push(cb); 13 | }; 14 | 15 | Factory.prototype.build = function(dataset, props, opts) { 16 | // favour passed in properties to defined properties 17 | const properties = _.extend({}, this.properties, props); 18 | const options = opts || {}; 19 | 20 | const doc = {}; 21 | const setProp = function(subDoc, prop, value) { 22 | if (_.isFunction(value)) { 23 | setProp(subDoc, prop, value.call(doc, dataset)); 24 | } else if (value instanceof Factory) { 25 | if (options.noRelations) { 26 | setProp(subDoc, prop, Random.id()); 27 | } else { 28 | const relation = value.build(dataset); 29 | setProp(subDoc, prop, relation._id); 30 | } 31 | // TODO: what is the correct check here? 32 | } else if (_.isObject(value) && !_.isDate(value) && !_.isArray(value)) { 33 | subDoc[prop] = subDoc[prop] || {}; 34 | walk(subDoc[prop], value); // eslint-disable-line 35 | } else if (prop !== '_id') { 36 | const modifier = {$set: {}}; 37 | modifier.$set[prop] = value; 38 | LocalCollection._modify(subDoc, modifier); 39 | } 40 | }; 41 | 42 | // walk the tree and evaluate 43 | function walk(subDoc, subProps) { 44 | _.each(subProps, function(value, prop) { 45 | setProp(subDoc, prop, value); 46 | }); 47 | } 48 | 49 | // you can't set _id with _modify 50 | if (properties._id) { 51 | let id = properties._id; 52 | if (_.isFunction(id)) { 53 | id = id.call(doc); 54 | } 55 | doc._id = id; 56 | } else { 57 | doc._id = Random.id(); 58 | } 59 | 60 | walk(doc, properties); 61 | 62 | _.each(this.afterBuildCbs, function(callback) { 63 | callback(doc, dataset); 64 | }); 65 | 66 | dataset.addDocument(doc, this.collection); 67 | return doc; 68 | }; 69 | -------------------------------------------------------------------------------- /packages/factory/package.js: -------------------------------------------------------------------------------- 1 | Package.describe({ 2 | name: 'factory', 3 | version: '1.0.0', 4 | summary: 'Factories for Meteor', 5 | documentation: null, 6 | }); 7 | 8 | Package.onUse(function(api) { 9 | api.versionsFrom('1.1.0.2'); 10 | api.use([ 11 | 'underscore', 12 | 'minimongo', 13 | 'random', 14 | 'ecmascript' 15 | ]); 16 | api.imply(['dfischer:faker', 'random']); 17 | api.addFiles(['factory.js', 'dataset.js', 'factory-api.js']); 18 | api.export('Factory'); 19 | }); 20 | 21 | Package.onTest(function(api) { 22 | api.use(['ecmascript', 'tinytest', 'factory', 'underscore']); 23 | api.addFiles('factory-tests.js', 'server'); 24 | }); 25 | -------------------------------------------------------------------------------- /public/apple-touch-icon-precomposed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/meteor/module-todos-react/8bcd4a8856045e4d6997bf55474dedd161c3dc28/public/apple-touch-icon-precomposed.png -------------------------------------------------------------------------------- /public/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/meteor/module-todos-react/8bcd4a8856045e4d6997bf55474dedd161c3dc28/public/favicon.png -------------------------------------------------------------------------------- /public/font/OpenSans-Light-webfont.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/meteor/module-todos-react/8bcd4a8856045e4d6997bf55474dedd161c3dc28/public/font/OpenSans-Light-webfont.eot -------------------------------------------------------------------------------- /public/font/OpenSans-Light-webfont.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | This is a custom SVG webfont generated by Font Squirrel. 6 | Copyright : Digitized data copyright 20102011 Google Corporation 7 | Foundry : Ascender Corporation 8 | Foundry URL : httpwwwascendercorpcom 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | 197 | 198 | 199 | 200 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | 208 | 209 | 210 | 211 | 212 | 213 | 214 | 215 | 216 | 217 | 218 | 219 | 220 | 221 | 222 | 223 | 224 | 225 | 226 | 227 | 228 | 229 | 230 | 231 | 232 | 233 | 234 | 235 | 236 | 237 | 238 | 239 | 240 | 241 | 242 | 243 | 244 | 245 | 246 | 247 | 248 | 249 | 250 | 251 | 252 | -------------------------------------------------------------------------------- /public/font/OpenSans-Light-webfont.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/meteor/module-todos-react/8bcd4a8856045e4d6997bf55474dedd161c3dc28/public/font/OpenSans-Light-webfont.ttf -------------------------------------------------------------------------------- /public/font/OpenSans-Light-webfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/meteor/module-todos-react/8bcd4a8856045e4d6997bf55474dedd161c3dc28/public/font/OpenSans-Light-webfont.woff -------------------------------------------------------------------------------- /public/font/OpenSans-Regular-webfont.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/meteor/module-todos-react/8bcd4a8856045e4d6997bf55474dedd161c3dc28/public/font/OpenSans-Regular-webfont.eot -------------------------------------------------------------------------------- /public/font/OpenSans-Regular-webfont.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/meteor/module-todos-react/8bcd4a8856045e4d6997bf55474dedd161c3dc28/public/font/OpenSans-Regular-webfont.ttf -------------------------------------------------------------------------------- /public/font/OpenSans-Regular-webfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/meteor/module-todos-react/8bcd4a8856045e4d6997bf55474dedd161c3dc28/public/font/OpenSans-Regular-webfont.woff -------------------------------------------------------------------------------- /public/icon/todos.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/meteor/module-todos-react/8bcd4a8856045e4d6997bf55474dedd161c3dc28/public/icon/todos.eot -------------------------------------------------------------------------------- /public/icon/todos.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Generated by IcoMoon 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /public/icon/todos.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/meteor/module-todos-react/8bcd4a8856045e4d6997bf55474dedd161c3dc28/public/icon/todos.ttf -------------------------------------------------------------------------------- /public/icon/todos.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/meteor/module-todos-react/8bcd4a8856045e4d6997bf55474dedd161c3dc28/public/icon/todos.woff -------------------------------------------------------------------------------- /public/logo-todos.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 36 | 42 | 48 | 50 | 51 | 52 | -------------------------------------------------------------------------------- /server/main.js: -------------------------------------------------------------------------------- 1 | import '../imports/startup/server/register-api.js'; 2 | import '../imports/startup/server/fixtures.js'; 3 | import '../imports/startup/server/security.js'; 4 | --------------------------------------------------------------------------------