├── .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 |
145 |
146 |
147 | );
148 | }
149 |
150 | renderEditingHeader() {
151 | const { list } = this.props;
152 | return (
153 |
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 |
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 |
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 |
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 |
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 |
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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
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 |
--------------------------------------------------------------------------------