├── .eslintrc
├── .gitignore
├── .meteor
├── .finished-upgraders
├── .gitignore
├── .id
├── packages
├── platforms
├── release
└── versions
├── CONTRIBUTING.md
├── README.md
├── client
├── lib
│ ├── main.js
│ ├── spacebars.js
│ └── subscriptions.js
├── stylesheets
│ ├── base
│ │ └── base.import.less
│ ├── main.less
│ └── modules
│ │ ├── footer.import.less
│ │ ├── header.import.less
│ │ ├── jobs.import.less
│ │ ├── messages.import.less
│ │ ├── navigation.import.less
│ │ ├── posts.import.less
│ │ ├── useraccounts.import.less
│ │ └── users.import.less
└── views
│ ├── feed.html
│ ├── feed.js
│ ├── footer.html
│ ├── head.html
│ ├── header.html
│ ├── header.js
│ ├── jobBoard.html
│ ├── jobBoard.js
│ ├── layout.html
│ ├── layout.js
│ ├── messages.html
│ ├── messages.js
│ ├── shared
│ ├── _follow_button.html
│ ├── _follow_button.js
│ ├── _navigation.html
│ ├── _navigation.js
│ ├── _post.html
│ ├── _post.js
│ └── _small_profile.html
│ └── users
│ ├── browse_users.html
│ ├── browse_users.js
│ ├── follower.html
│ ├── follower.js
│ ├── following.html
│ ├── following.js
│ ├── profile.html
│ ├── profile.js
│ ├── update_profile.html
│ └── update_profile.js
├── collections
├── jobs.js
├── messages.js
├── posts.js
└── users.js
├── lib
├── accounts
│ └── config.js
├── avatars
│ └── config.js
└── router.js
├── license.md
├── public
└── img
│ ├── facebook.svg
│ ├── github.svg
│ ├── googlePlus.svg
│ ├── instagram.svg
│ ├── linkedin.svg
│ ├── logo-gravity.png
│ ├── twitter.svg
│ └── youtube.svg
├── screenshot-1.png
└── server
├── indexes.js
├── publications.js
└── startup.js
/.eslintrc:
--------------------------------------------------------------------------------
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 | "env": {
9 | "browser": true,
10 | "meteor": true,
11 | "node": true
12 | },
13 | "ecmaFeatures": {
14 | "arrowFunctions": true,
15 | "blockBindings": true,
16 | "classes": true,
17 | "defaultParams": true,
18 | "destructuring": true,
19 | "forOf": true,
20 | "generators": false,
21 | "modules": true,
22 | "objectLiteralComputedProperties": true,
23 | "objectLiteralDuplicateProperties": false,
24 | "objectLiteralShorthandMethods": true,
25 | "objectLiteralShorthandProperties": true,
26 | "restParams": true,
27 | "spread": true,
28 | "superInFunctions": true,
29 | "templateStrings": true,
30 | "jsx": true
31 | },
32 | "rules": {
33 |
34 | /**
35 | * Meteor Specific
36 | */
37 | // babel inserts "use strict"; for us
38 | // http://eslint.org/docs/rules/strict
39 | "strict": [2, "never"],
40 |
41 | // allows certain non-constructor functions to start with a capital letter
42 | "new-cap": [2, { // http://eslint.org/docs/rules/new-cap
43 | "capIsNewExceptions": [
44 | "Match", "Any", "Object", "ObjectIncluding", "OneOf", "Optional", "Where"
45 | ]
46 | }],
47 |
48 | /**
49 | * ES6 Specific
50 | */
51 | "arrow-parens": 0, // http://eslint.org/docs/rules/arrow-parens
52 | "arrow-spacing": 2, // http://eslint.org/docs/rules/arrow-spacing
53 | "constructor-super": 2, // http://eslint.org/docs/rules/constructor-super
54 | "generator-star-spacing": 2, // http://eslint.org/docs/rules/generator-star-spacing
55 | "no-class-assign": 2, // http://eslint.org/docs/rules/no-class-assign
56 | "no-const-assign": 2, // http://eslint.org/docs/rules/no-const-assign
57 | /* “no-dupe-class-members": 2,*/ // http://eslint.org/docs/rules/no-dupe-class-members
58 | "no-this-before-super": 2, // http://eslint.org/docs/rules/no-this-before-super
59 | "no-var": 2, // http://eslint.org/docs/rules/no-var
60 | "object-shorthand": 0, // http://eslint.org/docs/rules/object-shorthand
61 | /* “prefer-arrow-callback": 1,*/ // http://eslint.org/docs/rules/prefer-arrow-callback
62 | "prefer-const": 2, // http://eslint.org/docs/rules/prefer-const
63 | "prefer-spread": 2, // http://eslint.org/docs/rules/prefer-spread
64 | /* “prefer-template": 2,*/ // http://eslint.org/docs/rules/prefer-template
65 | "require-yield": 2, // http://eslint.org/docs/rules/require-yield
66 |
67 | /**
68 | * Variables
69 | */
70 | "no-shadow": 2, // http://eslint.org/docs/rules/no-shadow
71 | "no-shadow-restricted-names": 2, // http://eslint.org/docs/rules/no-shadow-restricted-names
72 | "no-unused-vars": [2, { // http://eslint.org/docs/rules/no-unused-vars
73 | "vars": "local",
74 | "args": "after-used"
75 | }],
76 | "no-use-before-define": [2, "nofunc"], // http://eslint.org/docs/rules/no-use-before-define
77 |
78 | /**
79 | * Possible errors
80 | */
81 | "block-scoped-var": 0, // http://eslint.org/docs/rules/block-scoped-var
82 | "no-alert": 1, // http://eslint.org/docs/rules/no-alert
83 | "no-cond-assign": [1, "always"], // http://eslint.org/docs/rules/no-cond-assign
84 | "no-console": 1, // http://eslint.org/docs/rules/no-console
85 | "no-constant-condition": 1, // http://eslint.org/docs/rules/no-constant-condition
86 | "no-debugger": 1, // http://eslint.org/docs/rules/no-debugger
87 | "no-dupe-keys": 2, // http://eslint.org/docs/rules/no-dupe-keys
88 | "no-duplicate-case": 2, // http://eslint.org/docs/rules/no-duplicate-case
89 | "no-empty": 2, // http://eslint.org/docs/rules/no-empty
90 | "no-empty-label": 2, // http://eslint.org/docs/rules/no-empty-label
91 | "no-ex-assign": 2, // http://eslint.org/docs/rules/no-ex-assign
92 | "no-extra-boolean-cast": 0, // http://eslint.org/docs/rules/no-extra-boolean-cast
93 | "no-extra-semi": 2, // http://eslint.org/docs/rules/no-extra-semi
94 | "no-func-assign": 2, // http://eslint.org/docs/rules/no-func-assign
95 | "no-inner-declarations": 2, // http://eslint.org/docs/rules/no-inner-declarations
96 | "no-invalid-regexp": 2, // http://eslint.org/docs/rules/no-invalid-regexp
97 | "no-irregular-whitespace": 2, // http://eslint.org/docs/rules/no-irregular-whitespace
98 | "no-obj-calls": 2, // http://eslint.org/docs/rules/no-obj-calls
99 | "no-sparse-arrays": 2, // http://eslint.org/docs/rules/no-sparse-arrays
100 | "no-undef": 2, // http://eslint.org/docs/rules/no-undef
101 | "no-unreachable": 2, // http://eslint.org/docs/rules/no-unreachable
102 | "quote-props": [1, "as-needed"], // http://eslint.org/docs/rules/quote-props
103 | "use-isnan": 2, // http://eslint.org/docs/rules/use-isnan
104 |
105 | /**
106 | * Best practices
107 | */
108 | "consistent-return": 2, // http://eslint.org/docs/rules/consistent-return
109 | "curly": [2, "multi-line"], // http://eslint.org/docs/rules/curly
110 | "default-case": 2, // http://eslint.org/docs/rules/default-case
111 | "dot-notation": [2, { // http://eslint.org/docs/rules/dot-notation
112 | "allowKeywords": true
113 | }],
114 | "eqeqeq": 2, // http://eslint.org/docs/rules/eqeqeq
115 | "guard-for-in": 2, // http://eslint.org/docs/rules/guard-for-in
116 | "max-len": [1, 100, 2, { // http://eslint.org/docs/rules/max-len
117 | "ignoreUrls": true, "ignorePattern": "['\"]"
118 | }],
119 | "no-caller": 2, // http://eslint.org/docs/rules/no-caller
120 | "no-else-return": 2, // http://eslint.org/docs/rules/no-else-return
121 | "no-eq-null": 2, // http://eslint.org/docs/rules/no-eq-null
122 | "no-eval": 2, // http://eslint.org/docs/rules/no-eval
123 | "no-extend-native": 2, // http://eslint.org/docs/rules/no-extend-native
124 | "no-extra-bind": 2, // http://eslint.org/docs/rules/no-extra-bind
125 | "no-fallthrough": 2, // http://eslint.org/docs/rules/no-fallthrough
126 | "no-floating-decimal": 2, // http://eslint.org/docs/rules/no-floating-decimal
127 | "no-implied-eval": 2, // http://eslint.org/docs/rules/no-implied-eval
128 | "no-iterator": 2, // http://eslint.org/docs/rules/no-iterator
129 | "no-label-var": 2, // http://eslint.org/docs/rules/no-label-var
130 | "no-lone-blocks": 2, // http://eslint.org/docs/rules/no-lone-blocks
131 | "no-loop-func": 2, // http://eslint.org/docs/rules/no-loop-func
132 | "no-multi-spaces": 2, // http://eslint.org/docs/rules/no-multi-spaces
133 | "no-multi-str": 2, // http://eslint.org/docs/rules/no-multi-str
134 | "no-native-reassign": 2, // http://eslint.org/docs/rules/no-native-reassign
135 | "no-new": 2, // http://eslint.org/docs/rules/no-new
136 | "no-new-func": 2, // http://eslint.org/docs/rules/no-new-func
137 | "no-new-wrappers": 2, // http://eslint.org/docs/rules/no-new-wrappers
138 | "no-octal": 2, // http://eslint.org/docs/rules/no-octal
139 | "no-octal-escape": 2, // http://eslint.org/docs/rules/no-octal-escape
140 | "no-param-reassign": 0, // http://eslint.org/docs/rules/no-param-reassign
141 | "no-process-exit": 2, // http://eslint.org/docs/rules/no-process-exit
142 | "no-proto": 2, // http://eslint.org/docs/rules/no-proto
143 | "no-redeclare": 2, // http://eslint.org/docs/rules/no-redeclare
144 | "no-return-assign": 2, // http://eslint.org/docs/rules/no-return-assign
145 | "no-script-url": 2, // http://eslint.org/docs/rules/no-script-url
146 | "no-self-compare": 2, // http://eslint.org/docs/rules/no-self-compare
147 | "no-sequences": 2, // http://eslint.org/docs/rules/no-sequences
148 | "no-throw-literal": 2, // http://eslint.org/docs/rules/no-throw-literal
149 | "no-with": 2, // http://eslint.org/docs/rules/no-with
150 | "radix": 2, // http://eslint.org/docs/rules/radix
151 | "vars-on-top": 0, // http://eslint.org/docs/rules/vars-on-top
152 | "wrap-iife": [2, "any"], // http://eslint.org/docs/rules/wrap-iife
153 | "yoda": 2, // http://eslint.org/docs/rules/yoda
154 |
155 | /**
156 | * Style
157 | */
158 | "indent": [2, 2], // http://eslint.org/docs/rules/indent
159 | "brace-style": [1, // http://eslint.org/docs/rules/brace-style
160 | "1tbs", {
161 | "allowSingleLine": true
162 | }],
163 | "quotes": [
164 | 1, "single", "avoid-escape" // http://eslint.org/docs/rules/quotes
165 | ],
166 | "camelcase": [2, { // http://eslint.org/docs/rules/camelcase
167 | "properties": "never"
168 | }],
169 | "comma-spacing": [2, { // http://eslint.org/docs/rules/comma-spacing
170 | "before": false,
171 | "after": true
172 | }],
173 | "comma-style": [2, "last"], // http://eslint.org/docs/rules/comma-style
174 | "eol-last": 2, // http://eslint.org/docs/rules/eol-last
175 | "func-names": 0, // http://eslint.org/docs/rules/func-names
176 | "func-style": 0, // http://eslint.org/docs/rules/func-style
177 | "key-spacing": [2, { // http://eslint.org/docs/rules/key-spacing
178 | "beforeColon": false,
179 | "afterColon": true
180 | }],
181 | "new-parens": 1, // http://eslint.org/docs/rules/new-parens
182 | "no-multiple-empty-lines": [2, { // http://eslint.org/docs/rules/no-multiple-empty-lines
183 | "max": 2
184 | }],
185 | "no-nested-ternary": 2, // http://eslint.org/docs/rules/no-nested-ternary
186 | "no-new-object": 2, // http://eslint.org/docs/rules/no-new-object
187 | "no-array-constructor": 2, // http://eslint.org/docs/rules/no-array-constructor
188 | "no-spaced-func": 2, // http://eslint.org/docs/rules/no-spaced-func
189 | "no-trailing-spaces": 2, // http://eslint.org/docs/rules/no-trailing-spaces
190 | "no-extra-parens": 2, // http://eslint.org/docs/rules/no-extra-parens
191 | "no-underscore-dangle": 0, // http://eslint.org/docs/rules/no-underscore-dangle
192 | "one-var": 0, // http://eslint.org/docs/rules/one-var
193 | "padded-blocks": [2, "never"], // http://eslint.org/docs/rules/padded-blocks
194 | "semi": [2, "always"], // http://eslint.org/docs/rules/semi
195 | "semi-spacing": [2, { // http://eslint.org/docs/rules/semi-spacing
196 | "before": false,
197 | "after": true
198 | }],
199 | "space-after-keywords": 2, // http://eslint.org/docs/rules/space-after-keywords
200 | "space-before-blocks": 2, // http://eslint.org/docs/rules/space-before-blocks
201 | "space-before-function-paren": [ // http://eslint.org/docs/rules/space-before-function-paren
202 | 2, "never"
203 | ],
204 | "space-infix-ops": 2, // http://eslint.org/docs/rules/space-infix-ops
205 | "space-return-throw-case": 2, // http://eslint.org/docs/rules/space-return-throw-case
206 | "space-unary-ops": 2, // http://eslint.org/docs/rules/space-unary-ops
207 | "spaced-comment": 2 // http://eslint.org/docs/rules/spaced-comment
208 | }
209 | }
210 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | ################ Meteor ################
2 | .meteor/local
3 | packages/*
4 |
5 | ################ Sublime Text ################
6 | # workspace files are user-specific
7 | *.sublime-workspace
8 |
9 | # project files should be checked into the repository, unless a significant
10 | # proportion of contributors will probably not be using SublimeText
11 | # *.sublime-project
12 |
13 | #sftp configuration file
14 | sftp-config.json
15 |
16 | ################ JetBrains ################
17 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm
18 |
19 | ## Directory-based project format
20 | .idea/
21 | # if you remove the above rule, at least ignore user-specific stuff:
22 | # .idea/workspace.xml
23 | # .idea/tasks.xml
24 | # and these sensitive or high-churn files:
25 | # .idea/dataSources.ids
26 | # .idea/dataSources.xml
27 | # .idea/sqlDataSources.xml
28 | # .idea/dynamic.xml
29 |
30 | ## File-based project format
31 | *.ipr
32 | *.iws
33 | *.iml
34 |
35 | ## Additional for IntelliJ
36 | out/
37 |
38 | # generated by mpeltonen/sbt-idea plugin
39 | .idea_modules/
40 |
41 | # generated by JIRA plugin
42 | atlassian-ide-plugin.xml
43 |
44 | # generated by Crashlytics plugin (for Android Studio and Intellij)
45 | com_crashlytics_export_strings.xml
46 |
47 | ################ Mac OS X ################
48 | .DS_Store
49 | .AppleDouble
50 | .LSOverride
51 |
52 | # Icon must end with two \r
53 | Icon
54 |
55 |
56 | # Thumbnails
57 | ._*
58 |
59 | # Files that might appear on external disk
60 | .Spotlight-V100
61 | .Trashes
62 |
63 | # Directories potentially created on remote AFP share
64 | .AppleDB
65 | .AppleDesktop
66 | Network Trash Folder
67 | Temporary Items
68 | .apdisk
69 |
70 | ################ Linux ################
71 | *~
72 |
73 | # KDE directory preferences
74 | .directory
75 |
76 | ################ Windows ################
77 | # Windows image file caches
78 | Thumbs.db
79 | ehthumbs.db
80 |
81 | # Folder config file
82 | Desktop.ini
83 |
84 | # Recycle Bin used on file shares
85 | $RECYCLE.BIN/
86 |
87 | # Windows Installer files
88 | *.cab
89 | *.msi
90 | *.msm
91 | *.msp
92 |
--------------------------------------------------------------------------------
/.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 | 16aaz3z194r0rr8u8tk9
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 | blaze-html-templates # Compile .html files into Meteor Blaze views
11 | session # Client-side reactive dictionary for your app
12 | jquery # Helpful client-side library
13 | tracker # Meteor's client-side reactive programming library
14 |
15 | standard-minifiers # JS/CSS minifiers run for production mode
16 | es5-shim # ECMAScript 5 compatibility for older browsers.
17 | ecmascript # Enable ECMAScript2015+ syntax in app code
18 |
19 | check
20 | audit-argument-checks
21 | skeleton:skeleton
22 | accounts-base
23 | accounts-password
24 | useraccounts:unstyled
25 | useraccounts:flow-routing
26 | kadira:flow-router
27 | kadira:blaze-layout
28 | arillo:flow-router-helpers
29 | themeteorchef:bert
30 | reactive-var
31 | momentjs:moment
32 | reywood:publish-composite
33 | fortawesome:fontawesome
34 | utilities:avatar
35 | tmeasday:publish-counts
36 | verron:autosize
37 | mizzao:user-status
38 | dburles:eslint
39 | kevohagan:sweetalert
40 | mizzao:autocomplete
41 | markdown
42 | simple:highlight.js
43 | less
44 |
--------------------------------------------------------------------------------
/.meteor/platforms:
--------------------------------------------------------------------------------
1 | server
2 | browser
3 |
--------------------------------------------------------------------------------
/.meteor/release:
--------------------------------------------------------------------------------
1 | METEOR@1.2.1
2 |
--------------------------------------------------------------------------------
/.meteor/versions:
--------------------------------------------------------------------------------
1 | accounts-base@1.2.2
2 | accounts-password@1.1.4
3 | arillo:flow-router-helpers@0.4.5
4 | audit-argument-checks@1.0.4
5 | autoupdate@1.2.4
6 | babel-compiler@5.8.24_1
7 | babel-runtime@0.1.4
8 | base64@1.0.4
9 | binary-heap@1.0.4
10 | blaze@2.1.3
11 | blaze-html-templates@1.0.1
12 | blaze-tools@1.0.4
13 | boilerplate-generator@1.0.4
14 | caching-compiler@1.0.0
15 | caching-html-compiler@1.0.2
16 | callback-hook@1.0.4
17 | check@1.1.0
18 | coffeescript@1.0.11
19 | cosmos:browserify@0.5.1
20 | dandv:caret-position@2.1.1
21 | dburles:eslint@1.0.1
22 | ddp@1.2.2
23 | ddp-client@1.2.1
24 | ddp-common@1.2.2
25 | ddp-rate-limiter@1.0.0
26 | ddp-server@1.2.2
27 | deps@1.0.9
28 | diff-sequence@1.0.1
29 | ecmascript@0.1.6
30 | ecmascript-runtime@0.2.6
31 | ejson@1.0.7
32 | email@1.0.8
33 | es5-shim@4.1.14
34 | fastclick@1.0.7
35 | fortawesome:fontawesome@4.4.0
36 | fourseven:scss@3.3.3_3
37 | geojson-utils@1.0.4
38 | hot-code-push@1.0.0
39 | html-tools@1.0.5
40 | htmljs@1.0.5
41 | http@1.1.1
42 | id-map@1.0.4
43 | jparker:crypto-core@0.1.0
44 | jparker:crypto-md5@0.1.1
45 | jparker:gravatar@0.3.1
46 | jquery@1.11.4
47 | kadira:blaze-layout@2.2.0
48 | kadira:flow-router@2.8.0
49 | kevohagan:sweetalert@1.0.0
50 | launch-screen@1.0.4
51 | less@2.5.1
52 | livedata@1.0.15
53 | localstorage@1.0.5
54 | logging@1.0.8
55 | markdown@1.0.5
56 | meteor@1.1.10
57 | meteor-base@1.0.1
58 | meteorhacks:inject-initial@1.0.3
59 | minifiers@1.1.7
60 | minimongo@1.0.10
61 | mizzao:autocomplete@0.5.1
62 | mizzao:timesync@0.3.4
63 | mizzao:user-status@0.6.6
64 | mobile-experience@1.0.1
65 | mobile-status-bar@1.0.6
66 | momentjs:moment@2.10.6
67 | mongo@1.1.3
68 | mongo-id@1.0.1
69 | npm-bcrypt@0.7.8_2
70 | npm-mongo@1.4.39_1
71 | observe-sequence@1.0.7
72 | ordered-dict@1.0.4
73 | promise@0.5.1
74 | random@1.0.5
75 | rate-limit@1.0.0
76 | reactive-dict@1.1.3
77 | reactive-var@1.0.6
78 | reload@1.1.4
79 | retry@1.0.4
80 | reywood:publish-composite@1.4.2
81 | routepolicy@1.0.6
82 | service-configuration@1.0.5
83 | session@1.1.1
84 | sha@1.0.4
85 | simple:highlight.js@1.2.0
86 | skeleton:skeleton@2.0.4
87 | softwarerero:accounts-t9n@1.1.4
88 | spacebars@1.0.7
89 | spacebars-compiler@1.0.7
90 | srp@1.0.4
91 | standard-minifiers@1.0.2
92 | templating@1.1.5
93 | templating-tools@1.0.0
94 | themeteorchef:bert@2.1.0
95 | tmeasday:publish-counts@0.7.2
96 | tracker@1.0.9
97 | ui@1.0.8
98 | underscore@1.0.4
99 | url@1.0.5
100 | useraccounts:core@1.12.4
101 | useraccounts:flow-routing@1.12.4
102 | useraccounts:unstyled@1.12.4
103 | utilities:avatar@0.9.2
104 | verron:autosize@3.0.8
105 | webapp@1.2.3
106 | webapp-hashing@1.0.5
107 | zimme:active-route@2.3.2
108 |
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | # Contributing
2 | Please review this guideline when contributing to the Gravity repository.
3 |
4 | ## Issues
5 | Issues should include a descriptive title and a description which briefly describes what the issue is all about.
6 | Issues can be anything such as bugs, features, enhancements, discussions...
7 |
8 | Before opening a new issue please search through all the existing ones so that we don't introduce duplicates.
9 |
10 | ## Pull Requests
11 | Each pull request should close / fix an issue so that the community can discuss the functionality
12 | before a line of code is written.
13 |
14 | We need to define some standards in order to keep code quality high.
15 | Pull requests who don't match the following standards won't be merged.
16 |
17 | We stick to the [Meteor Style Guides](https://github.com/meteor/meteor/wiki/Meteor-Style-Guide) as close as possible.
18 |
19 | Just read through the code a little bit to get an idea of the used coding conventions.
20 |
21 | Here are some important examples you should keep an eye on:
22 | - Use the new ES6 syntax as often as possible (especially try to avoid `var`)
23 | - camelCase syntax for variables (e.g. `userProfile`)
24 | - underscores for file names (e.g. `example_file.html`)
25 | - Each file should have an empty newline at the bottom ("Ensure line feed at end of file")
26 | - Use two level space indentation (no tab indentation)
27 | - Indent your code so that it's readable and sticks to the indentation of the overall project
28 | - Remove all `console.log` statements / unnecessary code
29 | - Try to avoid committing commented out code
30 | - Remove dead code
31 | - Useful variable names (e.g. `let user;` instead of `let u;`)
32 | - Shared partials should have an underscore in front of them and be placed in a folder called `shared` (e.g. `profiles/shared/_button.html`)
33 | -2 space indents (setq js-indent-level 2)
34 | - Spaces, not literal tabs (setq-default indent-tabs-mode nil)
35 | - No trailing whitespace (setq-default show-trailing-whitespace t)
36 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Gravity
2 | [Gravity](http://joingravity.herokuapp.com/) is an open source social network built with Meteor.
3 |
4 | 
5 |
6 | The most recent version is deployed at [http://joingravity.herokuapp.com/](http://joingravity.herokuapp.com/). Meet us there!
7 |
8 | ## Setup
9 | 1. Clone / fork the repository
10 | 2. `cd` into the downloaded folder
11 | 3. run `meteor`
12 | 4. head over to `http://localhost:3000`
13 |
14 | ## How can I help?
15 | Any kind of contribution is welcomed!
16 | Take a look at our [issues](https://github.com/GravityProject/gravity/issues) to get an overview where you can help.
17 |
18 | ## Contributors
19 | [Here](https://github.com/GravityProject/gravity/graphs/contributors) is the list of all the contributors so far. Thank you!
20 |
--------------------------------------------------------------------------------
/client/lib/main.js:
--------------------------------------------------------------------------------
1 | setTitle = (title) => {
2 | let base = 'Gravity';
3 | if (title) {
4 | document.title = title + ' - ' + base;
5 | }
6 | };
7 |
--------------------------------------------------------------------------------
/client/lib/spacebars.js:
--------------------------------------------------------------------------------
1 | UI.registerHelper('simpleFormat', (text) => {
2 | if (!text) {
3 | return;
4 | }
5 | var carriage_returns, linkify, newline, paragraphs;
6 | linkify = (string) => {
7 | var re;
8 | re = [
9 | "\\b((?:https?|ftp)://[^\\s\"'<>]+)\\b",
10 | "\\b(www\\.[^\\s\"'<>]+)\\b",
11 | "\\b(\\w[\\w.+-]*@[\\w.-]+\\.[a-z]{2,6})\\b",
12 | "@([a-z0-9]+)"
13 | ];
14 | re = new RegExp(re.join('|'), 'gi');
15 | return string.replace(re, (match, url, www, mail, username) => {
16 | if (url) {
17 | return '' + url + '';
18 | }
19 | if (www) {
20 | return '' + www + '';
21 | }
22 | if (mail) {
23 | return '' + mail + '';
24 | }
25 | if (username) {
26 | if (Meteor.user().username === username) {
27 | return '@' + username + '';
28 | }
29 | return '@' + username;
30 | }
31 | return match;
32 | });
33 | };
34 | text = linkify(text);
35 | carriage_returns = /\r\n?/g;
36 | paragraphs = /\n\n+/g;
37 | newline = /([^\n]\n)(?=[^\n])/g;
38 | text = text.replace(carriage_returns, '\n');
39 | text = text.replace(paragraphs, '
\n\n');
40 | text = text.replace(newline, '$1
');
41 | text = '
' + text + '
';
42 | return new Spacebars.SafeString(text);
43 | });
44 |
45 | UI.registerHelper('formatDate', (date) => {
46 | return moment(date).format('LL');
47 | });
48 |
49 | /*
50 | * Full time format
51 | */
52 | UI.registerHelper('fullTimeDate', (date) => {
53 | return moment(date).format('HH:mm:ss, LL');
54 | });
55 |
56 | Template.registerHelper('instance', () => Template.instance());
57 |
--------------------------------------------------------------------------------
/client/lib/subscriptions.js:
--------------------------------------------------------------------------------
1 | Meteor.subscribe('userStatus');
2 |
--------------------------------------------------------------------------------
/client/stylesheets/base/base.import.less:
--------------------------------------------------------------------------------
1 | a {
2 | text-decoration: none;
3 | &:hover {
4 | text-decoration: underline;
5 | }
6 | }
7 |
8 | .text-center {
9 | text-align: center;
10 | }
11 |
12 | .no-button {
13 | border: 0;
14 | padding: 0;
15 | margin-bottom: 0;
16 | margin-left: 10px;
17 | }
18 |
19 | .alert {
20 | padding: 15px;
21 | text-align: center;
22 | margin-bottom: 20px;
23 | &.info {
24 | background: #D9EDF7;
25 | color: #31708F;
26 | }
27 | &.danger {
28 | background: #F2DEDE;
29 | color: #A94442;
30 | }
31 | &.success {
32 | background: #DFF0D8;
33 | color: #3C763D;
34 | }
35 | &.warning {
36 | background: #FCF8E3;
37 | color: #8A6D3B;
38 | }
39 | }
40 |
41 | .button-danger {
42 | background: #DB1E1E;
43 | color: #FFFFFF;
44 | border-color: #DB1E1E;
45 | &:hover, &:focus {
46 | color: #FFFFFF;
47 | background: darken(#DB1E1E, 10%);
48 | border-color: darken(#DB1E1E, 10%);
49 | }
50 | }
51 |
52 | .clear {
53 | clear: both;
54 | }
55 |
--------------------------------------------------------------------------------
/client/stylesheets/main.less:
--------------------------------------------------------------------------------
1 | @import 'base/base.import.less';
2 |
3 | @import 'modules/header.import.less';
4 | @import 'modules/navigation.import.less';
5 | @import 'modules/posts.import.less';
6 | @import 'modules/users.import.less';
7 | @import 'modules/footer.import.less';
8 | @import 'modules/messages.import.less';
9 | @import 'modules/jobs.import.less';
10 |
11 | // keep it at the bottom
12 | @import 'modules/useraccounts.import.less';
13 |
--------------------------------------------------------------------------------
/client/stylesheets/modules/footer.import.less:
--------------------------------------------------------------------------------
1 | footer.footer {
2 | border-top: 1px solid #EEEEEE;
3 | padding: 10px;
4 | margin-top: 20px;
5 | }
6 |
--------------------------------------------------------------------------------
/client/stylesheets/modules/header.import.less:
--------------------------------------------------------------------------------
1 | header.main-header {
2 | display: block;
3 | width: 100%;
4 | height: 6.5rem;
5 | background: #FFFFFF;
6 | z-index: 99;
7 | border-top: 1px solid #EEEEEE;
8 | border-bottom: 1px solid #EEEEEE;
9 | margin-bottom: 20px;
10 | ul {
11 | list-style: none;
12 | margin-bottom: 0;
13 | li {
14 | position: relative;
15 | float: left;
16 | margin-bottom: 0;
17 | margin-right: 10px;
18 | button {
19 | border: 0;
20 | padding: 0;
21 | line-height: 6.5rem;
22 | }
23 | a {
24 | text-transform: uppercase;
25 | font-size: 11px;
26 | font-weight: 600;
27 | letter-spacing: .2rem;
28 | text-decoration: none;
29 | line-height: 6.5rem;
30 | color: #222222;
31 | }
32 | }
33 | li.u-pull-right {
34 | float: right;
35 | }
36 | li.sign-out {
37 | margin-right: 0;
38 | }
39 | }
40 | .logo {
41 | float: left;
42 | padding: 11px 0;
43 | height: 43px;
44 | width: auto;
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/client/stylesheets/modules/jobs.import.less:
--------------------------------------------------------------------------------
1 | [data-id="addJob-form"] {
2 | .addJob-single-line {
3 | height: 38px;
4 | min-height: 38px;
5 | }
6 | .addJob-multi-line {
7 | height: 110px;
8 | min-height: 110px;
9 | }
10 | [data-id="addJob-submit"].disabled,
11 | [data-id="addJob-submit"].disabled:hover {
12 | opacity: 0.3;
13 | cursor: default;
14 | background-color: #33C3F0;
15 | border-color: #33C3F0;
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/client/stylesheets/modules/messages.import.less:
--------------------------------------------------------------------------------
1 | #allMessagesArea,
2 | #singleMessageContainer {
3 | border: 1px solid #D1D1D1;
4 | border-radius: 4px;
5 | box-shadow: none;
6 | min-width: 200px;
7 | min-height: 100px;
8 | height: 450px;
9 | max-height: 450px;
10 | }
11 |
12 | #allMessagesArea {
13 | overflow: scroll;
14 | tr {
15 | td:first-child {
16 | padding-left: 10px;
17 | }
18 | td:nth-child(4) {
19 | padding-right: 10px;
20 | }
21 | }
22 | }
23 |
24 | #singleMessageContainer {
25 | overflow: hidden;
26 | }
27 |
28 | .emptyMessage {
29 | display: flex;
30 | justify-content: center;
31 | align-items: center;
32 | height: 100%;
33 | opacity: 0.8;
34 | }
35 |
36 | #messagesButtons {
37 | display: flex;
38 | justify-content: space-between;
39 | }
40 |
41 | #singleMessageHeaderArea {
42 | height: 8%;
43 | var:first-child {
44 | margin-left: 20px;
45 | height: 100%;
46 | display: flex;
47 | align-items: center;
48 | }
49 | hr {
50 | margin-top: 0px;
51 | margin-bottom: 15px;
52 | }
53 | }
54 |
55 | #singleMessageArea {
56 | overflow: scroll;
57 | height: 59%;
58 |
59 | }
60 |
61 | .smallCol {
62 | width: 20%;
63 | max-width: 120px;
64 | overflow: hidden;
65 | white-space: nowrap;
66 | text-overflow: ellipsis;
67 | }
68 |
69 | .largeCol {
70 | width: 50%;
71 | max-width: 275px;
72 | overflow: hidden;
73 | white-space: nowrap;
74 | text-overflow: ellipsis;
75 | }
76 |
77 | .individualMessage {
78 | p {
79 | border-radius: 10%;
80 | font-style: normal;
81 | font-size: 14px;
82 | padding: 4px;
83 | margin-bottom: 0px;
84 | display: block;
85 | position: relative;
86 | clear: both;
87 | margin: 0px 20px;
88 | }
89 | &.left {
90 | p {
91 | border: 1px solid #cfcfcf;
92 | float: left;
93 | }
94 | }
95 | &.right {
96 | p {
97 | background-color: #cfcfcf;
98 | float: right;
99 | }
100 | }
101 | }
102 |
103 | .individualMessageDate {
104 | font-size: 12px;
105 | display: block;
106 | position: relative;
107 | clear: both;
108 | margin: 0px 25px 10px 25px;
109 | &.left {
110 | float: left;
111 | }
112 | &.right {
113 | float: right;
114 | }
115 | }
116 |
117 | #allMessagesButton.active,
118 | #composeButton.active {
119 | background: rgba(0, 0, 0, 0.1);
120 | }
121 |
122 | #allMessagesArea table {
123 | width: 100%;
124 | }
125 |
126 | td {
127 | padding: 0px;
128 | }
129 |
130 | .notRead {
131 | font-weight: bold;
132 | }
133 |
134 | #singleMessageReplyArea {
135 | [data-id="reply-body"] {
136 | min-height: 100px;
137 | height: 100px;
138 | margin-bottom: 0px;
139 | }
140 | [data-id="reply-submit"] {
141 | margin-bottom: 0px;
142 | }
143 | [data-id="reply-submit"].disabled,
144 | [data-id="reply-submit"].disabled:hover {
145 | opacity: 0.3;
146 | cursor: default;
147 | background-color: #33C3F0;
148 | border-color: #33C3F0;
149 | }
150 | }
151 |
152 | .sweet-alert {
153 | button {
154 | padding-top: 0px;
155 | }
156 | }
157 |
158 | [data-id="send-message-form"] {
159 | .-autocomplete-container {
160 | margin-top: 15px;
161 | }
162 | [data-id="message-submit"].disabled,
163 | [data-id="message-submit"].disabled:hover {
164 | opacity: 0.3;
165 | cursor: default;
166 | background-color: #33C3F0;
167 | border-color: #33C3F0;
168 | }
169 | }
--------------------------------------------------------------------------------
/client/stylesheets/modules/navigation.import.less:
--------------------------------------------------------------------------------
1 | .navigation {
2 | ul {
3 | margin-bottom: 0;
4 | list-style: none;
5 | a:hover {
6 | text-decoration: none;
7 | }
8 | li {
9 | padding: 5px;
10 | &.active {
11 | background: #EEEEEE;
12 | font-weight: bold;
13 | a {
14 | text-decoration: none;
15 | }
16 | }
17 | }
18 | }
19 | hr {
20 | margin-top: 20px;
21 | margin-bottom: 20px;
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/client/stylesheets/modules/posts.import.less:
--------------------------------------------------------------------------------
1 | .post {
2 | padding: 10px;
3 | border: 1px solid #EEEEEE;
4 | overflow-wrap: break-word;
5 | word-wrap: break-word;
6 | hr {
7 | margin-top: 15px;
8 | margin-bottom: 10px;
9 | }
10 | p.formatted-date, a.author {
11 | color: #D5D5D5;
12 | }
13 | p {
14 | margin-bottom: 0;
15 | }
16 | .avatar {
17 | margin-right: 5px;
18 | }
19 | .highlight-username {
20 | background: #FFEDCA;
21 | }
22 | margin-bottom: 20px;
23 | .post-line-two {
24 | display: flex;
25 | flex-direction: row;
26 | justify-content: space-between;
27 | .post-line-two-left,
28 | .post-line-two-right {
29 | display: flex;
30 | align-items: center;
31 | }
32 | .fa-thumbs-o-up {
33 | margin-right: 2px;
34 | &.liked {
35 | color: #1EAEDB;
36 | }
37 | }
38 | .remove-post {
39 | margin-left: 0px;
40 | margin-right: 10px;
41 | }
42 | [data-id="remove-post"],
43 | [data-id="like-post"] {
44 | font-size: 14px;
45 | }
46 | }
47 |
48 | }
49 |
50 | form.search-posts-form {
51 | margin-bottom: 0;
52 | input {
53 | margin-bottom: 0;
54 | }
55 | }
56 |
57 | ul.feed-filter {
58 | list-style: none;
59 | margin-bottom: 0;
60 | display: inline-block;
61 | li {
62 | float: left;
63 | margin-bottom: 0;
64 | margin-right: 10px;
65 | button {
66 | margin-bottom: 0;
67 | &.active {
68 | background: rgba(0, 0, 0, .1);
69 | }
70 | }
71 | &:last-child {
72 | margin-right: 0;
73 | }
74 | }
75 | }
76 |
77 | textarea {
78 | &.insert-post-form {
79 | min-height: 39px;
80 | }
81 | &.message-body-form {
82 | min-height: 200px;
83 | }
84 | }
85 |
86 | [data-id="insert-post-form"] {
87 | .-autocomplete-container {
88 | margin-top: 15px;
89 | }
90 | input[type="submit"].disabled,
91 | input[type="submit"].disabled:hover {
92 | opacity: 0.3;
93 | cursor: default;
94 | background-color: #33C3F0;
95 | border-color: #33C3F0;
96 | }
97 | }
98 |
--------------------------------------------------------------------------------
/client/stylesheets/modules/useraccounts.import.less:
--------------------------------------------------------------------------------
1 | .at-title h3 {
2 | font-size: 5rem;
3 | font-weight: 300;
4 | line-height: 1.2;
5 | letter-spacing: -0.1rem;
6 | text-align: center;
7 | }
8 |
9 | .at-input input {
10 | width: 100%;
11 | }
12 |
13 | .at-btn.submit {
14 | color: #FFF;
15 | background-color: #33C3F0 !important;
16 | outline: 0px none;
17 | display: inline-block;
18 | width: 100%;
19 | height: 38px;
20 | padding: 0px 30px;
21 | text-align: center;
22 | font-size: 11px;
23 | font-weight: 600;
24 | line-height: 38px;
25 | letter-spacing: 0.1rem;
26 | text-transform: uppercase;
27 | text-decoration: none;
28 | white-space: nowrap;
29 | background-color: transparent;
30 | border-radius: 4px;
31 | border: 1px solid #33C3F0; cursor: pointer;
32 | box-sizing: border-box;
33 | }
34 | .at-error {
35 | text-align: center;
36 | color: #D43939;
37 | }
38 |
--------------------------------------------------------------------------------
/client/stylesheets/modules/users.import.less:
--------------------------------------------------------------------------------
1 | .profile {
2 | .avatar-center {
3 | margin: 0 auto;
4 | }
5 |
6 | h1.username {
7 | margin-top: 20px;
8 | margin-bottom: 5px;
9 | font-size: 3.0rem;
10 | }
11 |
12 | #socialMediaAccounts {
13 | display: flex;
14 | justify-content: space-around;
15 | .smAccount {
16 | &:hover {
17 | transform: scale(1.2);
18 | }
19 | img {
20 | width: 75px;
21 | height: 75px;
22 | margin-top: -15px;
23 | }
24 | }
25 | }
26 |
27 | textarea.socialMedia {
28 | min-height: 40px;
29 | height: 40px;
30 | margin-bottom: 10px;
31 | }
32 |
33 | textarea.biography {
34 | height: 200px;
35 | }
36 |
37 | input[type="submit"].disabled,
38 | input[type="submit"].disabled:hover {
39 | opacity: 0.3;
40 | cursor: default;
41 | background-color: #33C3F0;
42 | border-color: #33C3F0;
43 | }
44 | }
45 |
46 | .small-profile {
47 | padding: 10px;
48 | border: 1px solid #EEEEEE;
49 | margin-bottom: 20px;
50 | .avatar, p {
51 | margin-right: 20px;
52 | }
53 | }
54 |
55 | .presence {
56 | &.online {
57 | margin-left: 5px;
58 | color: green;
59 | }
60 |
61 | &.offline {
62 | margin-left: 5px;
63 | color: gray;
64 | }
65 | }
66 |
--------------------------------------------------------------------------------
/client/views/feed.html:
--------------------------------------------------------------------------------
1 |
2 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
29 |
30 |
31 | {{#each posts}}
32 | {{> post post=this}}
33 | {{/each}}
34 | {{#if Template.subscriptionsReady}}
35 | {{#unless instance.postsCount.get}}
36 |
There are no matching posts to display
37 | {{/unless}}
38 | {{#if hasMorePosts}}
39 |
40 | {{/if}}
41 | {{else}}
42 |
Loading posts...
43 | {{/if}}
44 |
45 |
46 |
47 |
--------------------------------------------------------------------------------
/client/views/feed.js:
--------------------------------------------------------------------------------
1 | Template.feed.onCreated(function() {
2 | this.searchQuery = new ReactiveVar('');
3 | this.filter = new ReactiveVar('all');
4 | this.limit = new ReactiveVar(20);
5 | this.postsCount = new ReactiveVar(0);
6 |
7 | this.autorun(() => {
8 | this.subscribe('posts.all', this.searchQuery.get(), this.filter.get(), this.limit.get());
9 | this.subscribe('users.all', this.searchQuery.get(), this.limit.get());
10 | this.postsCount.set(Counts.get('posts.all'));
11 | });
12 | });
13 |
14 | Template.feed.onRendered(() => {
15 | autosize($('[data-id=body]'));
16 |
17 | // Set submit button to disabled since text field is empty
18 | $('input[type=submit]').addClass('disabled');
19 | });
20 |
21 | Template.feed.helpers({
22 | posts: () => {
23 | const instance = Template.instance();
24 | if (instance.searchQuery.get()) {
25 | return Posts.find({}, { sort: [['score', 'desc']] });
26 | }
27 | return Posts.find({}, { sort: { createdAt: -1 } });
28 | },
29 |
30 | activeIfFilterIs: (filter) => {
31 | if (filter === Template.instance().filter.get()) {
32 | return 'active';
33 | }
34 | },
35 |
36 | hasMorePosts: () => {
37 | return Template.instance().limit.get() <= Template.instance().postsCount.get();
38 | },
39 | // Settings for autocomplete in post field
40 | settings: () => {
41 | return {
42 | position: 'bottom',
43 | limit: 5,
44 | rules: [{
45 | token: '@',
46 | collection: Meteor.users,
47 | field: 'username',
48 | template: Template.userList,
49 | filter: { _id: { $ne: Meteor.userId() }}
50 | }]
51 | };
52 | }
53 | });
54 |
55 | Template.feed.events({
56 | 'keyup [data-id=body]': (event, template) => {
57 | // If body section has text enable the submit button, else disable it
58 | if (template.find('[data-id=body]').value.toString().trim() !== '') {
59 | $('input[type=submit]').removeClass('disabled');
60 | } else {
61 | $('input[type=submit]').addClass('disabled');
62 | }
63 |
64 | // When shift and enter are pressed, submit form
65 | if (event.shiftKey && event.keyCode === 13) {
66 | $('[data-id=insert-post-form]').submit();
67 | }
68 | },
69 |
70 | 'submit [data-id=insert-post-form]': (event, template) => {
71 | event.preventDefault();
72 |
73 | // Only continue if button isn't disabled
74 | if (!$('input[type=submit]').hasClass('disabled')) {
75 | let body = template.find('[data-id=body]').value;
76 |
77 | // If a user is mentioned in the post add span with class to highlight their username
78 | if(body.indexOf('@') !== -1) {
79 | for(let x = 0; x < body.length; x++) {
80 | if(body[x] === '@') {
81 | let u = body.slice(x + 1, body.indexOf(' ', x));
82 | let mentionedUser = Meteor.users.findOne({username: u});
83 |
84 | // If a valid user
85 | if(mentionedUser) {
86 | // Add opening and closing span tags
87 | body = body.slice(0, x) + '' + body.slice(x, body.indexOf(' ', x)) + '' +
88 | body.slice(body.indexOf(' ', x));
89 |
90 | // Increment by number of characters in openeing span tag
91 | // so the same mention doesn't get evaluated multiple times
92 | x+= 16;
93 | }
94 | }
95 | }
96 | }
97 |
98 | Meteor.call('posts.insert', body, (error, result) => {
99 | if (error) {
100 | Bert.alert(error.reason, 'danger', 'growl-top-right');
101 | } else {
102 | Bert.alert('Post successfully submitted', 'success', 'growl-top-right');
103 | template.find('[data-id=body]').value = '';
104 | $('[data-id=body]').css('height', '39px');
105 | $('input[type=submit]').addClass('disabled');
106 | }
107 | });
108 | }
109 | },
110 |
111 | 'click [data-id=all]': (event, template) => {
112 | template.filter.set('all');
113 | },
114 |
115 | 'click [data-id=following]': (events, template) => {
116 | template.filter.set('following');
117 | },
118 |
119 | 'click [data-id=load-more]': (event, template) => {
120 | template.limit.set(template.limit.get() + 20);
121 | },
122 |
123 | 'keyup [data-id=search-query]': _.debounce((event, template) => {
124 | event.preventDefault();
125 | template.searchQuery.set(template.find('[data-id=search-query]').value);
126 | template.limit.set(20);
127 | }, 300),
128 |
129 | 'submit [data-id=search-posts-form]': (event, template) => {
130 | event.preventDefault();
131 | }
132 | });
133 |
--------------------------------------------------------------------------------
/client/views/footer.html:
--------------------------------------------------------------------------------
1 |
2 |
16 |
17 |
--------------------------------------------------------------------------------
/client/views/head.html:
--------------------------------------------------------------------------------
1 |
2 |
3 | Gravity
4 |
5 |
--------------------------------------------------------------------------------
/client/views/header.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | -
6 |
7 |
8 |
9 |
10 | {{#if currentUser}}
11 | -
12 |
13 |
14 | {{/if}}
15 |
16 |
17 |
18 |
19 |
--------------------------------------------------------------------------------
/client/views/header.js:
--------------------------------------------------------------------------------
1 | Template.header.events({
2 | 'click [data-id=sign-out]': function() {
3 | Meteor.logout(function(error) {
4 | if (error) {
5 | alert(error.reason);
6 | } else {
7 | FlowRouter.go('/sign-in');
8 | }
9 | });
10 | }
11 | });
12 |
--------------------------------------------------------------------------------
/client/views/jobBoard.html:
--------------------------------------------------------------------------------
1 |
2 | {{#if showAllJobs}}
3 |
4 |
5 |
6 |
9 |
10 |
11 |
12 |
13 | {{#each jobs}}
14 |
15 | {{title}}
16 | {{location}}
17 | {{formatDate createdOn}}
18 | {{#if isAuthor author}}
19 |
22 |
25 | {{/if}}
26 |
27 | {{/each}}
28 | {{#if Template.subscriptionsReady}}
29 | {{#unless instance.jobsCount.get}}
30 |
There are no matching jobs to display
31 | {{/unless}}
32 | {{#if hasMoreJobs}}
33 |
34 | {{/if}}
35 | {{else}}
36 |
Loading jobs...
37 | {{/if}}
38 |
39 |
40 | {{else}}
41 | {{#if showSingleJob}}
42 | {{> singleJob}}
43 | {{else}}
44 | {{> addJob}}
45 | {{/if}}
46 | {{/if}}
47 |
48 |
49 |
50 |
51 | {{#with getSingleJob}}
52 | {{title}}
53 | {{schedule}}
54 | Date posted: {{formatDate createdOn}}
55 | Location: {{location}}
56 | Description:
57 | {{description}}
58 | Qualifications:
59 | {{qualifications}}
60 | Responsibilities:
61 | {{responsibilities}}
62 | {{#if notAuthor author}}
63 |
64 | {{/if}}
65 | {{/with}}
66 |
67 |
68 |
69 |
70 |
95 |
96 |
--------------------------------------------------------------------------------
/client/views/jobBoard.js:
--------------------------------------------------------------------------------
1 | /* Function to check if view passed in is the current view */
2 | function isCurrentView(v) {
3 | if (Session.get('currentView') === v) {
4 | return true;
5 | } else {
6 | return false;
7 | }
8 | }
9 |
10 | /* jobBoard template onCreated */
11 | Template.jobBoard.onCreated(function() {
12 | this.searchQuery = new ReactiveVar('');
13 | this.limit = new ReactiveVar(10);
14 | this.jobsCount = new ReactiveVar(0);
15 | this.usersCount = new ReactiveVar(0);
16 |
17 | this.autorun(() => {
18 | this.subscribe('jobs.all', this.searchQuery.get(), this.limit.get());
19 | this.jobsCount.set(Counts.get('jobs.all'));
20 |
21 | this.subscribe('users.all', this.searchQuery.get(), this.limit.get());
22 | this.usersCount.set(Counts.get('users.all'));
23 |
24 | // Show allJobs view
25 | Session.set('currentView', 'allJobs');
26 | Session.set('selectedJob', '');
27 | });
28 | });
29 |
30 | /* jobBoard template helpers */
31 | Template.jobBoard.helpers({
32 | showAllJobs: () => {
33 | return isCurrentView('allJobs');
34 | },
35 | showSingleJob: () => {
36 | return isCurrentView('singleJob');
37 | },
38 | showAddJob: () => {
39 | return isCurrentView('addJob') || isCurrentView('editJob');
40 | },
41 | jobs: () => {
42 | // Get all jobs, recently posted ones first
43 | return Jobs.find({}, {sort: {createdOn: -1 } });
44 | },
45 | hasMoreJobs: () => {
46 | return Template.instance().limit.get() <= Template.instance().jobsCount.get();
47 | },
48 | formatDate: (date) => {
49 | let currDate = moment(new Date()),
50 | postedDate = moment(new Date(date));
51 |
52 | let diff = currDate.diff(postedDate, 'days');
53 |
54 | if (diff === 0 && currDate.day() === postedDate.day()) {
55 | return 'Today';
56 | } else if (diff < 30) {
57 | if (diff <= 1) {
58 | return '1 day ago';
59 | } else {
60 | return diff + ' days ago';
61 | }
62 | } else {
63 | return '30+ days ago';
64 | }
65 | },
66 | isAuthor: (author) => {
67 | return Meteor.userId() === author;
68 | }
69 | });
70 |
71 | /* jobBoard template events */
72 | Template.jobBoard.events({
73 | 'click [data-id=load-more]': (event, template) => {
74 | template.limit.set(template.limit.get() + 20);
75 | },
76 | 'keyup [data-id=search-jobs-query]': _.debounce((event, template) => {
77 | event.preventDefault();
78 |
79 | template.searchQuery.set(template.find('[data-id=search-jobs-query]').value);
80 | template.limit.set(20);
81 | }, 300),
82 | 'submit [data-id=search-jobs-form]': (event, template) => {
83 | event.preventDefault();
84 | },
85 | 'click #postJobButton': (event, template) => {
86 | Session.set('currentView', 'addJob');
87 | },
88 | 'click .small-profile': (event, template) => {
89 | Session.set('selectedJob', event.currentTarget.id);
90 | Session.set('currentView', 'singleJob');
91 | },
92 | 'click [data-id=edit-job]': (event, template) => {
93 | event.stopPropagation();
94 |
95 | Session.set('selectedJob', event.currentTarget.parentNode.id);
96 | Session.set('currentView', 'editJob');
97 | },
98 | 'click [data-id=remove-job]': (event, template) => {
99 | event.stopPropagation();
100 |
101 | // Sweet Alert delete confirmation
102 | swal({
103 | title: 'Delete job?',
104 | text: 'Are you sure that you want to delete this job?',
105 | type: 'error',
106 | showCancelButton: true,
107 | closeOnConfirm: true,
108 | cancelButtonText: 'No',
109 | confirmButtonText: 'Yes, delete it!',
110 | confirmButtonColor: '#da5347'
111 | }, function() {
112 | // Get the id of the job to be deleted
113 | let jobId = event.currentTarget.parentNode.id;
114 |
115 | // Make sure message exists
116 | let job = Jobs.findOne({ _id: jobId } );
117 |
118 | // If message exists
119 | if (job) {
120 | // Remove selected job
121 | Meteor.call('jobs.remove', jobId, (error, result) => {
122 | if (error) {
123 | Bert.alert('Job couldn\'t be deleted.', 'danger', 'growl-top-right');
124 | } else {
125 | Bert.alert('Job deleted', 'success', 'growl-top-right');
126 | }
127 | });
128 | } else {
129 | Bert.alert('Job couldn\'t be deleted.', 'danger', 'growl-top-right');
130 | }
131 | });
132 | }
133 | });
134 |
135 | /* singleJob template helpers */
136 | Template.singleJob.helpers({
137 | getSingleJob: () => {
138 | return Jobs.findOne({ _id: Session.get('selectedJob') } );
139 | },
140 | formatDate: (date) => {
141 | return moment(date).format('MM/DD/YY');
142 | },
143 | notAuthor: (author) => {
144 | return Meteor.userId() !== author;
145 | }
146 | });
147 |
148 | /* singleJob template events */
149 | Template.singleJob.events({
150 | 'click .allJobsButton': (event, template) => {
151 | Session.set('currentView', 'allJobs');
152 | },
153 | 'click #applyNowButton': (event, template) => {
154 | // Get job details
155 | let currJob = Jobs.findOne({ _id: Session.get('selectedJob') } );
156 |
157 | if (currJob.externalLink && currJob.externalLink !== '') {
158 | // Proceed to external link
159 | let link = currJob.externalLink;
160 |
161 | if (link.toString().indexOf('http://') === -1) {
162 | link = 'http://' + link;
163 | }
164 |
165 | window.open(link);
166 | } else {
167 | // Send message to job poster
168 | let authorName = Meteor.users.findOne({_id: currJob.author.toString()}).username,
169 | msg = "Hi, I'm interested in the job you posted titled \"" + currJob.title + ".\"";
170 |
171 | if (currJob.author && authorName) {
172 | Meteor.call('messages.insert', currJob.author, authorName, msg, (error, result) => {
173 | if (error) {
174 | Bert.alert(error.reason, 'danger', 'growl-top-right');
175 | } else {
176 | // Display success message and reset form values
177 | Bert.alert('Message sent to the job poster.', 'success', 'growl-top-right');
178 |
179 | Session.set('currentView', 'allJobs');
180 | }
181 | });
182 | } else {
183 | Bert.alert('There was a problem sending the message.', 'danger', 'growl-top-right');
184 | }
185 | }
186 | }
187 | });
188 |
189 | /* addJob template onRendered */
190 | Template.addJob.onRendered(function() {
191 | $('[data-id=addJob-submit]').addClass('disabled');
192 | Session.set('showExternalLink', false);
193 |
194 | if (isCurrentView('editJob')) {
195 | let jobToEdit = Jobs.findOne({ _id: Session.get('selectedJob') } );
196 |
197 | if (jobToEdit.externalLink && jobToEdit.externalLink !== '') {
198 | Session.set('showExternalLink', true);
199 | $('[data-id=addJob-interestedBehavior]').val('goToLink');
200 | } else {
201 | Session.set('showExternalLink', false);
202 | $('[data-id=addJob-interestedBehavior]').val('directMessage');
203 | }
204 |
205 | // Fill in fields with existing data
206 | $('[data-id=addJob-title]').val(jobToEdit.title);
207 | $('[data-id=addJob-location]').val(jobToEdit.location);
208 | $('[data-id=addJob-description]').val(jobToEdit.description);
209 | $('[data-id=addJob-responsibilities]').val(jobToEdit.responsibilities);
210 | $('[data-id=addJob-qualifications]').val(jobToEdit.qualifications);
211 | $('[data-id=addJob-schedule]').val(jobToEdit.schedule);
212 | $('[data-id=addJob-externalLink]').val(jobToEdit.externalLink);
213 |
214 | // Keep track of original values
215 | Session.set('startingTitle', $('[data-id=addJob-title]').val());
216 | Session.set('startingLocation', $('[data-id=addJob-location]').val());
217 | Session.set('startingDescription', $('[data-id=addJob-description]').val());
218 | Session.set('startingResponsibilities', $('[data-id=addJob-responsibilities]').val());
219 | Session.set('startingQualifications', $('[data-id=addJob-qualifications]').val());
220 | Session.set('startingSchedule', $('[data-id=addJob-schedule]').val());
221 | Session.set('startingLink', $('[data-id=addJob-externalLink]').val());
222 |
223 | // Change button text
224 | $('[data-id=addJob-submit]').prop('value', 'Save');
225 | } else if (isCurrentView('addJob')) {
226 | // Change button text
227 | $('[data-id=addJob-submit]').prop('value', 'Post');
228 | }
229 | });
230 |
231 | /* addJob template helpers */
232 | Template.addJob.helpers({
233 | showExternalLink: () => {
234 | return Session.get('showExternalLink');
235 | }
236 | });
237 |
238 | /* addJob template events */
239 | Template.addJob.events({
240 | 'change [data-id=addJob-interestedBehavior]': (event, template) => {
241 | if (template.find('[data-id=addJob-interestedBehavior] option:selected').value === 'goToLink') {
242 | Session.set('showExternalLink', true);
243 | } else {
244 | Session.set('showExternalLink', false);
245 | }
246 | },
247 | 'click .allJobsButton': (event, template) => {
248 | Session.set('currentView', 'allJobs');
249 | },
250 | 'keyup [data-id=addJob-title], keyup [data-id=addJob-location], keyup [data-id=addJob-description], keyup [data-id=addJob-responsibilities], keyup [data-id=addJob-qualifications], change [data-id=addJob-schedule], change [data-id=addJob-interestedBehavior], keyup [data-id=addJob-externalLink]': (event, template) => {
251 | if (isCurrentView('addJob')) {
252 | try {
253 | // If job title, location, and description sections have text enable the submit button, else disable it
254 | if (template.find('[data-id=addJob-title]').value.toString().trim() !== '' &&
255 | template.find('[data-id=addJob-location]').value.toString().trim() !== '' &&
256 | template.find('[data-id=addJob-description]').value.toString().trim() !== '' &&
257 | (template.find('[data-id=addJob-interestedBehavior] option:selected').value.toString() === 'directMessage' ||
258 | (template.find('[data-id=addJob-interestedBehavior] option:selected').value.toString() === 'goToLink' &&
259 | template.find('[data-id=addJob-externalLink]').value.toString().trim() !== ''))) {
260 | $('[data-id=addJob-submit]').removeClass('disabled');
261 | } else {
262 | $('[data-id=addJob-submit]').addClass('disabled');
263 | }
264 | } catch(err) {
265 | $('[data-id=addJob-submit]').addClass('disabled');
266 | }
267 | } else if (isCurrentView('editJob')) {
268 | // If any of the values have changed enable the save button, else disable it
269 | if (template.find('[data-id=addJob-title]').value.toString().trim() !== Session.get('startingTitle') ||
270 | template.find('[data-id=addJob-location]').value.toString().trim() !== Session.get('startingLocation') ||
271 | template.find('[data-id=addJob-description]').value.toString().trim() !== Session.get('startingDescription') ||
272 | template.find('[data-id=addJob-responsibilities]').value.toString().trim() !== Session.get('startingResponsibilities') ||
273 | template.find('[data-id=addJob-qualifications]').value.toString().trim() !== Session.get('startingQualifications') ||
274 | template.find('[data-id=addJob-schedule]').value !== Session.get('startingSchedule') ||
275 | template.find('[data-id=addJob-externalLink]').value !== Session.get('startingLink')) {
276 | $('[data-id=addJob-submit]').removeClass('disabled');
277 | } else {
278 | $('[data-id=addJob-submit]').addClass('disabled');
279 | }
280 | }
281 | },
282 | 'submit [data-id=addJob-form]': (event, template) => {
283 | event.preventDefault();
284 |
285 | // Only continue if button isn't disabled
286 | if (!$('[data-id=addJob-submit]').hasClass('disabled')) {
287 | // Get values
288 | let title = template.find('[data-id=addJob-title]').value.toString().trim(),
289 | location = template.find('[data-id=addJob-location]').value.toString().trim(),
290 | schedule = template.find('[data-id=addJob-schedule] option:selected').text.trim(),
291 | description = template.find('[data-id=addJob-description]').value.toString().trim(),
292 | responsibilities = template.find('[data-id=addJob-responsibilities]').value.toString().trim(),
293 | qualifications = template.find('[data-id=addJob-qualifications]').value.toString().trim(),
294 | externalLink = '';
295 |
296 | if (template.find('[data-id=addJob-interestedBehavior] option:selected').value.toString() === 'goToLink') {
297 | externalLink = template.find('[data-id=addJob-externalLink]').value.toString().trim();
298 | }
299 |
300 | if (isCurrentView('addJob')) {
301 | // Title, location and description should have text
302 | if (title && location && description) {
303 | Meteor.call('jobs.post', title, location, schedule, description, responsibilities,
304 | qualifications, externalLink, (error, result) => {
305 | if (error) {
306 | Bert.alert(error.reason, 'danger', 'growl-top-right');
307 | } else {
308 | // Display success message
309 | Bert.alert('Job posted', 'success', 'growl-top-right');
310 |
311 | // Switch the to allJobs view
312 | Session.set('currentView', 'allJobs');
313 | }
314 | });
315 | } else {
316 | Bert.alert('Please enter a job title, location and description.', 'danger', 'growl-top-right');
317 | }
318 | } else if (isCurrentView('editJob')) {
319 | // Update existing job
320 | Meteor.call('jobs.update', Session.get('selectedJob'), title, location, schedule, description,
321 | responsibilities, qualifications, externalLink, (error, result) => {
322 | if (error) {
323 | Bert.alert(error.reason, 'danger', 'growl-top-right');
324 | } else {
325 | // Display success message
326 | Bert.alert('Job updated', 'success', 'growl-top-right');
327 |
328 | // Switch the to allJobs view
329 | Session.set('currentView', 'allJobs');
330 | }
331 | });
332 | }
333 | }
334 | }
335 | });
336 |
--------------------------------------------------------------------------------
/client/views/layout.html:
--------------------------------------------------------------------------------
1 |
2 | {{> header}}
3 |
4 | {{# if currentUser}}
5 |
6 |
7 | {{> navigation}}
8 |
9 |
10 | {{> Template.dynamic template=main}}
11 |
12 |
13 | {{else}}
14 |
15 |
16 | {{> Template.dynamic template=main}}
17 |
18 |
19 | {{/if}}
20 |
21 | {{> footer}}
22 |
23 |
--------------------------------------------------------------------------------
/client/views/layout.js:
--------------------------------------------------------------------------------
1 | let getHeight = () => {
2 | let windowHeight = $(window).height(),
3 | headerHeight = $('.main-header').outerHeight(true),
4 | footerHeight = $('.footer').outerHeight(true);
5 |
6 | return `${windowHeight - headerHeight - footerHeight}px`;
7 | };
8 |
9 | Template.layout.onCreated(function() {
10 | this.minHeight = new ReactiveVar(0);
11 | });
12 |
13 | Template.layout.onRendered(function() {
14 | this.minHeight.set(getHeight());
15 |
16 | $(window).resize(() => {
17 | this.minHeight.set(getHeight());
18 | });
19 | });
20 |
--------------------------------------------------------------------------------
/client/views/messages.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 | {{#if showAllMessages}}
9 | {{> allMessages}}
10 | {{else}}
11 | {{#if showSingleMessage}}
12 | {{> singleMessage}}
13 | {{else}}
14 | {{> compose}}
15 | {{/if}}
16 | {{/if}}
17 |
18 |
19 |
20 |
21 |
22 | {{#if messagesEmpty}}
23 |
None
24 | {{else}}
25 |
26 |
27 | {{#each getAllMessages}}
28 |
29 | {{getOtherUsername originatingFromName originatingToName}} |
30 | {{getMostRecentBody conversation}} |
31 | {{formatDate conversation}} |
32 | |
33 |
34 | {{/each}}
35 |
36 |
37 | {{/if}}
38 |
39 |
40 |
41 |
42 |
43 |
44 | {{#with getSingleMessage}}
45 |
49 |
50 | {{#each conversation}}
51 | {{#if showMessage originatingFromDeleted originatingToDeleted}}
52 | {{#if fromOtherUser from to}}
53 |
54 | {{body}}
55 | {{formatDate sentOn}}
56 |
57 | {{else}}
58 |
59 | {{body}}
60 | {{formatDate sentOn}}
61 |
62 | {{/if}}
63 | {{/if}}
64 | {{/each}}
65 |
66 | {{/with}}
67 |
68 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
84 |
85 |
86 |
87 |
88 |
89 | {{username}}
90 |
91 |
92 |
--------------------------------------------------------------------------------
/client/views/messages.js:
--------------------------------------------------------------------------------
1 | /* Function to check if view passed in is the current view */
2 | function isCurrentView(v) {
3 | if(Session.get('currentView') === v) {
4 | return true;
5 | } else {
6 | return false;
7 | }
8 | }
9 |
10 | /* Function to get the userId of the other user (not current user) */
11 | function getOtherUserId(fromId, toId) {
12 | if(fromId === Meteor.userId())
13 | {
14 | return toId;
15 | } else {
16 | return fromId;
17 | }
18 | }
19 |
20 | /* Function to get the username of the other user (not current user) */
21 | function getOtherUsername(fromName, toName) {
22 | if(fromName === Meteor.user().username)
23 | {
24 | Session.set('originating', 'from');
25 | return toName;
26 | } else {
27 | Session.set('originating', 'to');
28 | return fromName;
29 | }
30 | }
31 |
32 | /* Function to format the passed in date */
33 | function performFormatting(date) {
34 | let currDate = moment(new Date()),
35 | msgDate = moment(new Date(date));
36 |
37 | let diff = currDate.diff(msgDate, 'days');
38 |
39 | if(diff === 0 && currDate.day() === msgDate.day()) {
40 | return moment(date).format('h:mm a');
41 | } else if(diff < 7 && currDate.day() !== msgDate.day()) {
42 | return moment(date).format('dddd');
43 | } else {
44 | if(currDate.year() != msgDate.year()) {
45 | return moment(date).format('MM/DD/YY');
46 | } else {
47 | return moment(date).format('MMM DD');
48 | }
49 | }
50 | }
51 |
52 | /* Helper function available to all templates that calls getOtherUsername */
53 | Template.registerHelper('getOtherUsername', function(fromName, toName) {
54 | return getOtherUsername(fromName, toName);
55 | });
56 |
57 | /* On messages template created */
58 | Template.messages.onCreated(function () {
59 | this.searchQuery = new ReactiveVar('');
60 | this.limit = new ReactiveVar(20);
61 | this.usersCount = new ReactiveVar(0);
62 |
63 | this.autorun(() => {
64 | //Set subscriptions
65 | this.subscribe('users.all', this.searchQuery.get(), this.limit.get());
66 | this.usersCount.set(Counts.get('users.all'));
67 | this.subscribe('messages.all');
68 |
69 | //Set session variables
70 | Session.set('currentView', 'allMessages');
71 | Session.set('selectedMsg', '');
72 | Session.set('originating', '');
73 | $('#allMessagesButton').addClass('active');
74 | });
75 | });
76 |
77 | /* messages template helpers */
78 | Template.messages.helpers({
79 | showAllMessages: () => {
80 | return isCurrentView('allMessages');
81 | },
82 | showSingleMessage: () => {
83 | return isCurrentView('singleMessage');
84 | },
85 | showCompose: () => {
86 | return isCurrentView('compose');
87 | }
88 | });
89 |
90 | /* messages template events */
91 | Template.messages.events({
92 | 'click #allMessagesButton, click #composeButton': (event, template) => {
93 | //Set template based on button that was clicked
94 | $('button').removeClass('active');
95 | $('#' + event.target.id).addClass('active');
96 | Session.set('currentView', event.target.id.toString().replace('Button', ''));
97 | }
98 | });
99 |
100 | /* allMessages helpers */
101 | Template.allMessages.helpers({
102 | getAllMessages: () => {
103 | //Find all messages that current user is involved in
104 | return Messages.find({$or: [{ 'originatingFromId': Meteor.userId(), 'conversation.originatingFromDeleted': false }, {'originatingToId': Meteor.userId(), 'conversation.originatingToDeleted': false}]}, {sort: {'conversation.sentOn': -1} });
105 | },
106 | messagesEmpty: () => {
107 | //Check if current user has any messages
108 | if(Messages.find({$or: [{ 'originatingFromId': Meteor.userId(), 'conversation.originatingFromDeleted': false }, {'originatingToId': Meteor.userId(), 'conversation.originatingToDeleted': false}]}).count() === 0) {
109 | return true;
110 | } else {
111 | return false;
112 | }
113 | },
114 | formatDate: (conversation) => {
115 | if(conversation.length > 0)
116 | {
117 | let date = conversation[conversation.length - 1].sentOn;
118 |
119 | return performFormatting(date);
120 | }
121 | },
122 | getMostRecentBody: (conversation) => {
123 | if(conversation.length > 0) {
124 | return conversation[conversation.length - 1].body;
125 | }
126 | },
127 | getMostRecentRead: (conversation) => {
128 | //Determine if message has been read
129 | if(conversation.length > 0) {
130 | if(conversation[conversation.length - 1].to.userId === Meteor.userId()) {
131 | if(!conversation[conversation.length - 1].to.read) {
132 | return 'notRead';
133 | }
134 | }
135 | }
136 |
137 | return '';
138 | }
139 | });
140 |
141 | /* allMessages template events */
142 | Template.allMessages.events({
143 | 'click .msg': (event, template) => {
144 | //Make sure selected message exists
145 | let selectedMsg = Messages.findOne({_id: event.currentTarget.id});
146 | if(selectedMsg) {
147 | if(selectedMsg.conversation[selectedMsg.conversation.length - 1].to.userId === Meteor.userId()) {
148 | //Check if most recent message is TO the current user
149 | if(!selectedMsg.conversation[selectedMsg.conversation.length - 1].to.read) {
150 | //Update read field to true
151 | Meteor.call('messages.updateRead', selectedMsg._id, true, (error, result) => {
152 | //Do nothing
153 | });
154 | }
155 |
156 | }
157 | }
158 |
159 | //Set current view to singleMssage template
160 | $('button').removeClass('active');
161 | Session.set('selectedMsg', event.currentTarget.id);
162 | Session.set('currentView', 'singleMessage');
163 | },
164 | 'click .remove-message': (event, template) => {
165 | event.stopPropagation();
166 |
167 | //Sweet Alert delete confirmation
168 | swal({
169 | title: 'Delete message?',
170 | text: 'Are you sure that you want to delete this message?',
171 | type: 'error',
172 | showCancelButton: true,
173 | closeOnConfirm: true,
174 | cancelButtonText: 'No',
175 | confirmButtonText: 'Yes, delete it!',
176 | confirmButtonColor: '#da5347'
177 | }, function() {
178 | //Get the id of the message to be deleted
179 | let msgId = event.currentTarget.parentNode.parentNode.id;
180 |
181 | //Make sure message exists
182 | let msg = Messages.findOne({ '_id': msgId } );
183 | let whoDeleted;
184 |
185 | //If message exists
186 | if(msg) {
187 | //Set deleted correctly
188 | if(msg.originatingFromId === Meteor.userId()) {
189 | whoDeleted = 'from';
190 | } else {
191 | whoDeleted = 'to';
192 | }
193 |
194 | //Remove selected message
195 | Meteor.call('messages.remove', msgId, whoDeleted, (error, result) => {
196 | if(error) {
197 | Bert.alert('Message couldn\'t be deleted.', 'danger', 'growl-top-right');
198 | } else {
199 | Bert.alert('Message deleted', 'success', 'growl-top-right');
200 | }
201 | });
202 | } else {
203 | Bert.alert('Message couldn\'t be deleted.', 'danger', 'growl-top-right');
204 | }
205 | });
206 | }
207 | });
208 |
209 | /* singleMessage template on rendered */
210 | Template.singleMessage.onRendered(function() {
211 | //Move to bottom of div so most recent message is shown
212 | $('#singleMessageArea').scrollTop($('#singleMessageArea')[0].scrollHeight);
213 |
214 | //Set focus to the reply body text area
215 | $('[data-id=reply-body]').focus();
216 |
217 | //Set submit button to disabled since text field is empty
218 | $('[data-id=reply-submit]').addClass('disabled');
219 | });
220 |
221 | /* singleMessage template helpers */
222 | Template.singleMessage.helpers({
223 | getSingleMessage: () => {
224 | return Messages.findOne({ '_id': Session.get('selectedMsg') } );
225 | },
226 | showMessage: (originatingFromDeleted, originatingToDeleted) => {
227 | //Don't show messages that have been previously deleted
228 | if(Session.equals('originating', 'from')) {
229 | return !originatingFromDeleted;
230 | } else {
231 | return !originatingToDeleted;
232 | }
233 | },
234 | fromOtherUser: (from, to) => {
235 | if(from.userId !== Meteor.userId()) {
236 | return true;
237 | } else {
238 | return false;
239 | }
240 | },
241 | formatDate: (date) => {
242 | return performFormatting(date);
243 | }
244 | });
245 |
246 | /* singleMessage template events */
247 | Template.singleMessage.events({
248 | 'keyup [data-id=reply-body]': (event, template) => {
249 | //If reply section has text enable the submit button, else disable it
250 | if(template.find('[data-id=reply-body]').value.toString().trim() !== '') {
251 | $('[data-id=reply-submit]').removeClass('disabled');
252 | } else {
253 | $('[data-id=reply-submit]').addClass('disabled');
254 | }
255 |
256 | // When shift and enter are pressed, submit form
257 | if (event.shiftKey && event.keyCode === 13) {
258 | $('[data-id=reply-message-form]').submit();
259 | }
260 | },
261 | 'submit [data-id=reply-message-form]': (event, template) => {
262 | event.preventDefault();
263 |
264 | //Only continue if button isn't disabled
265 | if(!$('[data-id=reply-submit]').hasClass('disabled')) {
266 | let body = template.find('[data-id=reply-body]').value,
267 | currMessage = Messages.findOne({_id: Session.get('selectedMsg')}),
268 | toUserId = getOtherUserId(currMessage.originatingFromId, currMessage.originatingToId),
269 | fieldEmpty = false;
270 |
271 | //Make sure fields arent empty
272 | if(!body.toString().trim()) {
273 | fieldEmpty = true;
274 | Bert.alert('Please enter a message.', 'danger', 'growl-top-right');
275 | }
276 | if(!fieldEmpty && !toUserId) {
277 | fieldEmpty = true;
278 | Bert.alert('Error sending message.', 'danger', 'growl-top-right');
279 | }
280 |
281 | //Continue if the fields aren't empty
282 | if(!fieldEmpty) {
283 | //Add reply to the conversation array of the existing message
284 | Meteor.call('messages.addMessage', Session.get('selectedMsg'), toUserId, body, (error, result) => {
285 | if(error) {
286 | Bert.alert(error.reason, 'danger', 'growl-top-right');
287 | } else {
288 | //Display success message and reset form values
289 | Bert.alert('Message sent', 'success', 'growl-top-right');
290 | template.find('[data-id=reply-body]').value = '';
291 |
292 | //Scroll to bottom of message area, set focus back to reply text area, and disable submit button
293 | $('#singleMessageArea').scrollTop($('#singleMessageArea')[0].scrollHeight);
294 | $('[data-id=reply-body]').focus();
295 | $('[data-id=reply-submit]').addClass('disabled');
296 | }
297 | });
298 | }
299 | }
300 | }
301 | });
302 |
303 | /* compose template on rendered */
304 | Template.compose.onRendered(function() {
305 | //Set focus to the to text area
306 | $('[data-id=message-to]').focus();
307 |
308 | //Set submit button to disabled since text fields are empty
309 | $('[data-id=message-submit]').addClass('disabled');
310 | });
311 |
312 | /* compose template helpers */
313 | Template.compose.helpers({
314 | //Settings for autocomplete To field
315 | settings: () => {
316 | return {
317 | position: 'bottom',
318 | limit: 5,
319 | rules: [
320 | {
321 | collection: Meteor.users,
322 | field: 'username',
323 | template: Template.userList,
324 | filter: { _id: { $ne: Meteor.userId() }}
325 | }
326 | ]
327 | };
328 | }
329 | });
330 |
331 | /* compose template events */
332 | Template.compose.events({
333 | 'keyup [data-id=message-to], keyup [data-id=message-body]': (event, template) => {
334 | // If to and body sections have text enable the submit button, else disable it
335 | if (template.find('[data-id=message-to]').value.toString().trim() !== '' &&
336 | template.find('[data-id=message-body]').value.toString().trim() !== '') {
337 | $('[data-id=message-submit]').removeClass('disabled');
338 | } else {
339 | $('[data-id=message-submit]').addClass('disabled');
340 | }
341 | },
342 | 'keyup [data-id=message-body]': (event, template) => {
343 | // When shift and enter are pressed, submit form
344 | if (event.shiftKey && event.keyCode === 13) {
345 | $('[data-id=send-message-form]').submit();
346 | }
347 | },
348 | 'submit [data-id=send-message-form]': (event, template) => {
349 | event.preventDefault();
350 |
351 | //Only continue if button isn't disabled
352 | if(!$('[data-id=message-submit]').hasClass('disabled')) {
353 | //Get text from To and Body fields
354 | let to = template.find('[data-id=message-to]').value.toString().trim(),
355 | body = template.find('[data-id=message-body]').value,
356 | fieldEmpty = false;
357 |
358 | //Verify that both fields contain text
359 | if(!to) {
360 | fieldEmpty = true;
361 | Bert.alert('Please enter a username in the To field.', 'danger', 'growl-top-right');
362 | }
363 | if(!fieldEmpty && !body.toString().trim()) {
364 | fieldEmpty = true;
365 | Bert.alert('Please enter a message.', 'danger', 'growl-top-right');
366 | }
367 |
368 | //Continue if the fields aren't empty
369 | if(!fieldEmpty) {
370 | //Try to find user in Users collection
371 | let toUser = Meteor.users.findOne({username: to});
372 |
373 | //If user was found then it is a valid user
374 | if(toUser) {
375 | //If user is not the current user then send the message
376 | if(toUser._id != Meteor.userId())
377 | {
378 | //If message between these two users already exists, add this message to the current conversation, else create a new message
379 | let existingMessage = Messages.findOne({$or: [{'originatingFromId': Meteor.userId()}, {'originatingToId': Meteor.userId()}], $or: [{'originatingFromId': toUser._id}, {'originatingToId': toUser._id}]});
380 |
381 | if(existingMessage) {
382 | //Add message to existing conversation
383 | Meteor.call('messages.addMessage', existingMessage._id, toUser._id, body, (error, result) => {
384 | if(error) {
385 | Bert.alert(error.reason, 'danger', 'growl-top-right');
386 | } else {
387 | //Display success message and reset form values
388 | Bert.alert('Message sent', 'success', 'growl-top-right');
389 | template.find('[data-id=message-to]').value = '';
390 | template.find('[data-id=message-body]').value = '';
391 |
392 | //Switch the to allMessages view
393 | $('button').removeClass('active');
394 | $('#allMessagesButton').addClass('active');
395 | Session.set('currentView', 'allMessages');
396 | }
397 | });
398 | } else {
399 | //Create new message
400 | Meteor.call('messages.insert', toUser._id, toUser.username, body, (error, result) => {
401 | if(error) {
402 | Bert.alert(error.reason, 'danger', 'growl-top-right');
403 | } else {
404 | //Display success message and reset form values
405 | Bert.alert('Message sent', 'success', 'growl-top-right');
406 |
407 | //Switch the to allMessages view
408 | $('button').removeClass('active');
409 | $('#allMessagesButton').addClass('active');
410 | Session.set('currentView', 'allMessages');
411 | }
412 | });
413 | }
414 | } else {
415 | //Can't send a message to yourself, display error
416 | Bert.alert('Sorry, you can only send messages to other users.', 'danger', 'growl-top-right');
417 | }
418 | } else {
419 | //Wasn't a valid user, display error
420 | Bert.alert('Sorry, we can\'t find that user. Please verify the username and try again.', 'danger', 'growl-top-right');
421 | }
422 | }
423 | }
424 | }
425 | });
426 |
427 |
--------------------------------------------------------------------------------
/client/views/shared/_follow_button.html:
--------------------------------------------------------------------------------
1 |
2 | {{#if isThisUserNotCurrentUser}}
3 | {{#if isCurrentUserFollowingThisUser}}
4 |
5 | {{else}}
6 |
7 | {{/if}}
8 | {{/if}}
9 |
10 |
--------------------------------------------------------------------------------
/client/views/shared/_follow_button.js:
--------------------------------------------------------------------------------
1 | Template.followButton.events({
2 | 'click [data-id=follow]': function(event, template) {
3 | Meteor.call('users.follow', this.user._id, (error, result) => {
4 | if (error) {
5 | Bert.alert(error.reason, 'danger', 'growl-top-right');
6 | } else {
7 | Bert.alert(`You are now following @${this.user.username}`, 'success', 'growl-top-right');
8 | }
9 | });
10 | },
11 |
12 | 'click [data-id=unfollow]': function(event, template) {
13 | Meteor.call('users.unfollow', this.user._id, (error, result) => {
14 | if (error) {
15 | Bert.alert(error.reason, 'danger', 'growl-top-right');
16 | } else {
17 | Bert.alert(`You have unfollowed @${this.user.username}`, 'success', 'growl-top-right');
18 | }
19 | });
20 | }
21 | });
22 |
23 | Template.followButton.helpers({
24 | isThisUserNotCurrentUser: function() {
25 | return this.user._id !== Meteor.userId();
26 | },
27 |
28 | isCurrentUserFollowingThisUser: function() {
29 | return Meteor.user().followingIds && Meteor.user().followingIds.indexOf(this.user._id) > -1;
30 | }
31 | });
32 |
--------------------------------------------------------------------------------
/client/views/shared/_navigation.html:
--------------------------------------------------------------------------------
1 |
2 |
34 |
35 |
--------------------------------------------------------------------------------
/client/views/shared/_navigation.js:
--------------------------------------------------------------------------------
1 | /* On navigation template created */
2 | Template.navigation.onCreated(function() {
3 | this.autorun(() => {
4 | // Set subscriptions
5 | this.subscribe('messages.all');
6 | });
7 | });
8 |
9 | Template.navigation.helpers({
10 | activeIfRouteNameIs: (routeName) => {
11 | if (FlowRouter.getRouteName() === routeName) {
12 | return 'active';
13 | }
14 | return '';
15 | },
16 | getUnreadCount: () => {
17 | let unreadMessageCount = 0;
18 | let messages = Messages.find({$or: [{ originatingFromId: Meteor.userId(), 'conversation.originatingFromDeleted': false }, {originatingToId: Meteor.userId(), 'conversation.originatingToDeleted': false}]}).forEach(function(msg) {
19 | for (let x = 0; x < msg.conversation.length; x++) {
20 | if (msg.conversation[x].to.userId === Meteor.userId() && !msg.conversation[x].to.read) {
21 | unreadMessageCount++;
22 | }
23 | }
24 | });
25 |
26 | if (unreadMessageCount > 0) {
27 | return ('(' + unreadMessageCount + ')');
28 | } else {
29 | return '';
30 | }
31 | }
32 | });
33 |
--------------------------------------------------------------------------------
/client/views/shared/_post.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | {{#with post}}
5 |
6 | {{#markdown}}{{body}}{{/markdown}}
7 |
8 |
9 |
21 |
22 | {{#if belongsPostToUser}}
23 |
26 | {{/if}}
27 |
{{formatDate createdAt}}
28 |
29 |
30 |
31 | {{/with}}
32 |
33 |
34 |
35 |
--------------------------------------------------------------------------------
/client/views/shared/_post.js:
--------------------------------------------------------------------------------
1 | Template.post.onCreated(function() {
2 | this.searchQuery = new ReactiveVar('');
3 | this.filter = new ReactiveVar('all');
4 | this.limit = new ReactiveVar(20);
5 | this.autorun(() => {
6 | this.subscribe('posts.all', this.searchQuery.get(), this.filter.get(), this.limit.get());
7 | });
8 | });
9 |
10 | Template.post.events({
11 | 'click [data-id=remove-post]': function(event, template) {
12 | let self = this;
13 |
14 | // Sweet Alert delete confirmation
15 | swal({
16 | title: 'Delete post?',
17 | text: 'Are you sure that you want to delete this post?',
18 | type: 'error',
19 | showCancelButton: true,
20 | closeOnConfirm: true,
21 | cancelButtonText: 'No',
22 | confirmButtonText: 'Yes, delete it!',
23 | confirmButtonColor: '#da5347'
24 | }, function() {
25 | Meteor.call('posts.remove', self._id, (error, result) => {
26 | if (error) {
27 | Bert.alert(error.reason, 'danger', 'growl-top-right');
28 | } else {
29 | Bert.alert('Post successfully removed', 'success', 'growl-top-right');
30 | }
31 | });
32 | });
33 | },
34 | 'click [data-id=like-post]': function(event, template) {
35 | let self = this;
36 |
37 | Meteor.call('posts.like', self._id, (error, result) => {
38 | if (error) {
39 | Bert.alert(error.reason, 'danger', 'growl-top-right');
40 | }
41 | });
42 | }
43 | });
44 |
45 | Template.post.helpers({
46 | author: function() {
47 | return Meteor.users.findOne({ _id: this.authorId });
48 | },
49 | belongsPostToUser: function() {
50 | return this.authorId === Meteor.userId();
51 | },
52 | formatDate: function(date) {
53 | let currDate = moment(new Date()),
54 | msgDate = moment(new Date(date));
55 |
56 | let diff = currDate.diff(msgDate, 'days');
57 |
58 | if (diff === 0 && currDate.day() === msgDate.day()) {
59 | let hourDiff = currDate.diff(msgDate, 'hours'),
60 | minDiff = currDate.diff(msgDate, 'minutes');
61 | if (hourDiff > 0) {
62 | if (hourDiff === 1) {
63 | return (hourDiff + ' hr');
64 | } else {
65 | return (hourDiff + ' hrs');
66 | }
67 | } else if (minDiff > 0) {
68 | if (minDiff === 1) {
69 | return (minDiff + ' min');
70 | } else {
71 | return (minDiff + ' mins');
72 | }
73 | } else {
74 | return 'Just now';
75 | }
76 | } else if (diff <= 1 && currDate.day() !== msgDate.day()) {
77 | return ('Yesterday at ' + moment(date).format('h:mm a'));
78 | } else {
79 | if (currDate.year() !== msgDate.year()) {
80 | return moment(date).format('MMMM DD, YYYY');
81 | } else {
82 | return (moment(date).format('MMMM DD') + ' at ' + moment(date).format('h:mm a'));
83 | }
84 | }
85 | },
86 | isLiked: function() {
87 | if (Posts.find( { _id: this._id, already_voted: { $in: [Meteor.userId()]} }).count() === 1) {
88 | return 'liked';
89 | }
90 | return '';
91 | }
92 | });
93 |
--------------------------------------------------------------------------------
/client/views/shared/_small_profile.html:
--------------------------------------------------------------------------------
1 |
2 |
13 |
14 |
--------------------------------------------------------------------------------
/client/views/users/browse_users.html:
--------------------------------------------------------------------------------
1 |
2 |
9 |
10 |
11 | {{#each users}}
12 | {{> smallProfile user=this}}
13 | {{/each}}
14 | {{#if Template.subscriptionsReady}}
15 | {{#unless instance.usersCount.get}}
16 |
There are no matching users to display
17 | {{/unless}}
18 | {{#if hasMoreUsers}}
19 |
20 | {{/if}}
21 | {{else}}
22 |
Loading users...
23 | {{/if}}
24 |
25 |
26 |
27 |
--------------------------------------------------------------------------------
/client/views/users/browse_users.js:
--------------------------------------------------------------------------------
1 | Template.browseUsers.events({
2 | 'click [data-id=load-more]': (event, template) => {
3 | template.limit.set(template.limit.get() + 20);
4 | },
5 |
6 | 'keyup [data-id=search-query]': _.debounce((event, template) => {
7 | event.preventDefault();
8 | template.searchQuery.set(template.find('[data-id=search-query]').value);
9 | template.limit.set(20);
10 | }, 300),
11 |
12 | 'submit [data-id=search-users-form]': (event, template) => {
13 | event.preventDefault();
14 | }
15 | });
16 |
17 | Template.browseUsers.helpers({
18 | users: () => {
19 | return Meteor.users.find({ _id: { $ne: Meteor.userId() } }, { sort: { createdAt: -1 } });
20 | },
21 |
22 | hasMoreUsers: () => {
23 | return Template.instance().limit.get() <= Template.instance().usersCount.get();
24 | }
25 | });
26 |
27 | Template.browseUsers.onCreated(function() {
28 | this.searchQuery = new ReactiveVar('');
29 | this.limit = new ReactiveVar(20);
30 | this.usersCount = new ReactiveVar(0);
31 |
32 | this.autorun(() => {
33 | this.subscribe('users.all', this.searchQuery.get(), this.limit.get());
34 | this.usersCount.set(Counts.get('users.all'));
35 | });
36 | });
37 |
--------------------------------------------------------------------------------
/client/views/users/follower.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | {{#if Template.subscriptionsReady}}
5 | {{#if users.count}}
6 | {{#each users}}
7 | {{> smallProfile user=this}}
8 | {{/each}}
9 | {{else}}
10 |
There are no users to display
11 | {{/if}}
12 | {{else}}
13 |
Loading users...
14 | {{/if}}
15 |
16 |
17 |
18 |
--------------------------------------------------------------------------------
/client/views/users/follower.js:
--------------------------------------------------------------------------------
1 | Template.follower.helpers({
2 | users: () => {
3 | return Meteor.users.find({ _id: { $ne: Meteor.userId() } }, { sort: { username: 1 } });
4 | }
5 | });
6 |
7 | Template.follower.onCreated(function() {
8 | this.autorun(() => {
9 | this.subscribe('users.follower');
10 | });
11 | });
12 |
--------------------------------------------------------------------------------
/client/views/users/following.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | {{#if Template.subscriptionsReady}}
5 | {{#if users.count}}
6 | {{#each users}}
7 | {{> smallProfile user=this}}
8 | {{/each}}
9 | {{else}}
10 |
There are no users to display
11 | {{/if}}
12 | {{else}}
13 |
Loading users...
14 | {{/if}}
15 |
16 |
17 |
18 |
--------------------------------------------------------------------------------
/client/views/users/following.js:
--------------------------------------------------------------------------------
1 | Template.following.helpers({
2 | users: () => {
3 | if (Meteor.user().followingIds && Meteor.user().followingIds.length !== 0) {
4 | return Meteor.users.find({ _id: { $in: Meteor.user().followingIds } }, { sort: { username: 1 } });
5 | } else {
6 | return [];
7 | }
8 | }
9 | });
10 |
11 | Template.following.onCreated(function() {
12 | this.autorun(() => {
13 | this.subscribe('users.following');
14 | });
15 | });
16 |
--------------------------------------------------------------------------------
/client/views/users/profile.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | {{#with user}}
6 | {{> avatar user=user size='extra-large' shape='circle' class='avatar-center'}}
7 |
@{{user.username}}
8 |
9 |
10 |
{{simpleFormat user.biography}}
11 |
12 | {{> followButton user=this}}
13 |
14 | {{/with}}
15 |
16 | {{#each posts}}
17 | {{> post post=this}}
18 | {{/each}}
19 | {{#if Template.subscriptionsReady}}
20 | {{#unless instance.userPostsCount.get}}
21 |
The user has not posted anything yet
22 | {{/unless}}
23 | {{#if hasMorePosts}}
24 |
25 | {{/if}}
26 | {{else}}
27 |
Loading posts...
28 | {{/if}}
29 |
30 |
31 |
32 |
33 |
--------------------------------------------------------------------------------
/client/views/users/profile.js:
--------------------------------------------------------------------------------
1 | Template.profile.events({
2 | 'click [data-id=load-more]': (event, template) => {
3 | template.limit.set(template.limit.get() + 20);
4 | }
5 | });
6 |
7 | Template.profile.helpers({
8 | user: () => {
9 | return Meteor.users.findOne({ _id: FlowRouter.getParam('_id') });
10 | },
11 |
12 | posts: function() {
13 | return Posts.find({}, { sort: { createdAt: -1 } });
14 | },
15 |
16 | hasMorePosts: () => {
17 | return Template.instance().limit.get() <= Template.instance().userPostsCount.get();
18 | }
19 | });
20 |
21 | Template.profile.onCreated(function () {
22 | this.limit = new ReactiveVar(20);
23 | this.userPostsCount = new ReactiveVar(0);
24 |
25 | this.autorun(() => {
26 | this.subscribe('users.profile', FlowRouter.getParam('_id'), this.limit.get());
27 | this.userPostsCount.set(Counts.get('users.profile'));
28 |
29 | // Get current user's social media accounts
30 | let profileUser = Meteor.users.findOne({_id: FlowRouter.getParam('_id')});
31 |
32 | // Display social media links
33 | if (profileUser && profileUser.socialMedia) {
34 | $('#socialMediaAccounts').empty();
35 | for (var prop in profileUser.socialMedia) {
36 | let smLink = '
';
37 | $(smLink).appendTo('#socialMediaAccounts');
38 | }
39 | }
40 | });
41 | });
42 |
--------------------------------------------------------------------------------
/client/views/users/update_profile.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | {{> avatar user=currentUser size='extra-large' shape='circle' class='avatar-center'}}
6 |
@{{currentUser.username}}
7 |
8 |
9 |
15 |
16 |
17 |
18 |
19 |
--------------------------------------------------------------------------------
/client/views/users/update_profile.js:
--------------------------------------------------------------------------------
1 | Template.updateProfile.onCreated(function() {
2 | this.searchQuery = new ReactiveVar('');
3 | this.limit = new ReactiveVar(20);
4 | this.usersCount = new ReactiveVar(0);
5 |
6 | this.autorun(() => {
7 | // Set subscriptions
8 | this.subscribe('users.all', this.searchQuery.get(), this.limit.get());
9 | this.usersCount.set(Counts.get('users.all'));
10 |
11 | // Get current user's social media accounts
12 | let currentUser = Meteor.users.findOne({_id: Meteor.userId()});
13 |
14 | // Display social media links
15 | if (currentUser && currentUser.socialMedia) {
16 | $('#socialMediaAccounts').empty();
17 | for (var prop in currentUser.socialMedia) {
18 | let smLink = '
';
19 | $(smLink).appendTo('#socialMediaAccounts');
20 | }
21 | }
22 | });
23 | });
24 |
25 | Template.updateProfile.onRendered(function() {
26 | $('input[type=submit]').addClass('disabled');
27 | Session.set('startingBio', $('[data-id=biography]').val());
28 | });
29 |
30 | Template.updateProfile.events({
31 | 'keyup [data-id=biography], keyup [data-id=addSocialMedia]': (event, template) => {
32 | // If bio section has changed or social media section has text enable the submit button, else disable it
33 | if (template.find('[data-id=biography]').value !== Session.get('startingBio') ||
34 | template.find('[data-id=addSocialMedia]').value.toString().trim() !== '') {
35 | $('input[type=submit]').removeClass('disabled');
36 | } else {
37 | $('input[type=submit]').addClass('disabled');
38 | }
39 | },
40 |
41 | 'submit [data-id=update-profile-form]': (event, template) => {
42 | event.preventDefault();
43 |
44 | // Only continue if button isn't disabled
45 | if (!$('input[type=submit]').hasClass('disabled')) {
46 | let user = {
47 | biography: template.find('[data-id=biography]').value,
48 | socialMedia: { }
49 | };
50 |
51 | // Get current social media accounts for this user
52 | user.socialMedia = Meteor.users.findOne({_id: Meteor.userId()}).socialMedia || {};
53 |
54 | let bioChanged = false,
55 | numSmAdded = 0,
56 | numInvalidSm = 0;
57 | if (user.biography !== Session.get('startingBio')) {
58 | bioChanged = true;
59 | }
60 |
61 | // Add social media accounts if any were entered
62 | let newSocialMedia = template.find('[data-id=addSocialMedia]').value.toString().split(',');
63 | for (let x = 0; x < newSocialMedia.length; x++) {
64 | newSocialMedia[x] = newSocialMedia[x].trim();
65 |
66 | if (newSocialMedia[x] !== '') {
67 | if (newSocialMedia[x].indexOf('http://') === -1 && newSocialMedia[x].indexOf('https://') === -1) {
68 | newSocialMedia[x] = 'http://' + newSocialMedia[x];
69 | }
70 |
71 | if (newSocialMedia[x].toLowerCase().indexOf('facebook') !== -1) {
72 | user.socialMedia.facebook = newSocialMedia[x];
73 | numSmAdded++;
74 | } else if (newSocialMedia[x].toLowerCase().indexOf('twitter') !== -1) {
75 | user.socialMedia.twitter = newSocialMedia[x];
76 | numSmAdded++;
77 | } else if (newSocialMedia[x].toLowerCase().indexOf('linkedin') !== -1) {
78 | user.socialMedia.linkedin = newSocialMedia[x];
79 | numSmAdded++;
80 | } else if (newSocialMedia[x].toLowerCase().indexOf('instagram') !== -1) {
81 | user.socialMedia.instagram = newSocialMedia[x];
82 | numSmAdded++;
83 | } else if (newSocialMedia[x].toLowerCase().indexOf('plus.google') !== -1) {
84 | user.socialMedia.googlePlus = newSocialMedia[x];
85 | numSmAdded++;
86 | } else if (newSocialMedia[x].toLowerCase().indexOf('github') !== -1) {
87 | user.socialMedia.github = newSocialMedia[x];
88 | numSmAdded++;
89 | } else if (newSocialMedia[x].toLowerCase().indexOf('youtube') !== -1) {
90 | user.socialMedia.youtube = newSocialMedia[x];
91 | numSmAdded++;
92 | } else {
93 | numInvalidSm++;
94 | }
95 | }
96 | }
97 |
98 | if (bioChanged || numSmAdded > 0) {
99 | Meteor.call('users.updateProfile', user, (error, result) => {
100 | if (error) {
101 | Bert.alert(error.reason, 'danger', 'growl-top-right');
102 | } else {
103 | if (numInvalidSm > 0) {
104 | Bert.alert('Profile updated but some social media links were invalid', 'success', 'growl-top-right');
105 | } else {
106 | Bert.alert('Profile successfully updated', 'success', 'growl-top-right');
107 | }
108 |
109 | template.find('[data-id=addSocialMedia]').value = '';
110 | }
111 | });
112 | } else {
113 | Bert.alert('Invalid social media links', 'danger', 'growl-top-right');
114 | }
115 | }
116 | }
117 | });
118 |
--------------------------------------------------------------------------------
/collections/jobs.js:
--------------------------------------------------------------------------------
1 | Jobs = new Mongo.Collection('jobs');
2 |
3 | Meteor.methods({
4 | 'jobs.post': (title, location, schedule, description, responsibilities, qualifications, externalLink) => {
5 | check(title, String);
6 | check(location, String);
7 | check(schedule, String);
8 | check(description, String);
9 | check(responsibilities, String);
10 | check(qualifications, String);
11 | check(externalLink, String);
12 |
13 | // Verify that user is logged in
14 | if (!Meteor.user()) {
15 | throw new Meteor.Error(401, 'You need to be signed in to continue');
16 | }
17 |
18 | // Verify that required fields are empty
19 | if (!title) {
20 | throw new Meteor.Error(422, 'Job title should not be blank');
21 | }
22 | if (!location) {
23 | throw new Meteor.Error(422, 'Job location should not be blank');
24 | }
25 | if (!description) {
26 | throw new Meteor.Error(422, 'Job description should not be blank');
27 | }
28 |
29 | // Create job object to be inserted into DB
30 | let job = {
31 | title: title,
32 | location: location,
33 | schedule: schedule,
34 | description: description,
35 | responsibilities: responsibilities,
36 | qualifications: qualifications,
37 | externalLink: externalLink,
38 | author: Meteor.userId(),
39 | createdOn: new Date()
40 | };
41 |
42 | // Insert new job
43 | return Jobs.insert(job);
44 | },
45 | 'jobs.remove': (jobId) => {
46 | check(jobId, String);
47 |
48 | // Verify that user is logged in
49 | if (!Meteor.user()) {
50 | throw new Meteor.Error(401, 'You need to be signed in to continue');
51 | }
52 |
53 | // Verify that job exists
54 | if (Jobs.find({_id: jobId}).count() === 0) {
55 | throw new Meteor.Error(111, 'Not a valid job');
56 | }
57 |
58 | // Remove job by jobId
59 | return Jobs.remove({_id: jobId});
60 | },
61 | 'jobs.update': (jobId, title, location, schedule, description, responsibilities, qualifications, externalLink) => {
62 | check(jobId, String);
63 | check(title, String);
64 | check(location, String);
65 | check(schedule, String);
66 | check(description, String);
67 | check(responsibilities, String);
68 | check(qualifications, String);
69 | check(externalLink, String);
70 |
71 | // Verify that user is logged in
72 | if (!Meteor.user()) {
73 | throw new Meteor.Error(401, 'You need to be signed in to continue');
74 | }
75 |
76 | // Verify that job exists
77 | if (Jobs.find({_id: jobId}).count() === 0) {
78 | throw new Meteor.Error(111, 'Not a valid job');
79 | }
80 |
81 | // Update job by jobId
82 | return Jobs.update({_id: jobId}, {$set: {title: title, location: location, schedule: schedule,
83 | description: description, responsibilities: responsibilities,
84 | qualifications: qualifications, externalLink: externalLink}});
85 | }
86 | });
87 |
--------------------------------------------------------------------------------
/collections/messages.js:
--------------------------------------------------------------------------------
1 | Messages = new Mongo.Collection('messages');
2 |
3 | Meteor.methods({
4 | 'messages.insert': (toUserId, toUsername, body) => {
5 | check(toUserId, String);
6 | check(toUsername, String);
7 | check(body, String);
8 |
9 | // Verify that user is logged in
10 | if (!Meteor.user()) {
11 | throw new Meteor.Error(401, 'You need to be signed in to continue');
12 | }
13 |
14 | if (!toUserId) {
15 | throw new Meteor.Error(422, 'To field should not be blank');
16 | }
17 |
18 | if (!body) {
19 | throw new Meteor.Error(422, 'Body should not be blank');
20 | }
21 |
22 | // Verify that to is an existing user
23 | if (Meteor.users.find({_id: toUserId}).count() === 0) {
24 | throw new Meteor.Error(111, 'Not a valid user');
25 | }
26 |
27 | let message = {
28 | originatingFromId: Meteor.userId(),
29 | originatingFromName: Meteor.user().username,
30 | originatingToId: toUserId,
31 | originatingToName: toUsername,
32 | conversation: [{
33 | from: {
34 | userId: Meteor.userId()
35 | },
36 | to: {
37 | userId: toUserId,
38 | read: false
39 | },
40 | body: body,
41 | sentOn: new Date(),
42 | originatingFromDeleted: false,
43 | originatingToDeleted: false
44 | }]
45 | };
46 |
47 | return Messages.insert(message);
48 | },
49 | 'messages.remove': (messageId, whoDeleted) => {
50 | check(messageId, String);
51 | check(whoDeleted, String);
52 |
53 | // Verify that user is logged in
54 | if (!Meteor.user()) {
55 | throw new Meteor.Error(401, 'You need to be signed in to continue');
56 | }
57 |
58 | // Verify that message exists
59 | if (Messages.find({_id: messageId}).count() === 0) {
60 | throw new Meteor.Error(111, 'Not a valid message');
61 | }
62 |
63 | // Get conversation array and store in variable
64 | let conversation = Messages.findOne({_id: messageId}).conversation;
65 |
66 | if (whoDeleted === 'from') {
67 | for (let x = 0; x < conversation.length; x++) {
68 | conversation[x].originatingFromDeleted = true;
69 | }
70 | } else if (whoDeleted === 'to') {
71 | for (let x = 0; x < conversation.length; x++) {
72 | conversation[x].originatingToDeleted = true;
73 | }
74 | } else {
75 | throw new Meteor.Error(211, 'Message could not be deleted');
76 | }
77 |
78 | Messages.update({_id: messageId}, {$set: {conversation: conversation}});
79 | },
80 | 'messages.updateRead': (messageId, val) => {
81 | check(messageId, String);
82 | check(val, Boolean);
83 |
84 | // Verify that user is logged in
85 | if (!Meteor.user()) {
86 | throw new Meteor.Error(401, 'You need to be signed in to continue');
87 | }
88 |
89 | // Verify that message exists
90 | if (Messages.find({_id: messageId}).count() === 0) {
91 | throw new Meteor.Error(111, 'Not a valid message');
92 | }
93 |
94 | // Get conversation array and store in variable
95 | let conversation = Messages.findOne({_id: messageId}).conversation;
96 |
97 | for (let x = 0; x < conversation.length; x++) {
98 | if (conversation[x].to.userId === Meteor.userId()) {
99 | conversation[x].to.read = val;
100 | }
101 | }
102 |
103 | // Update entire conversation array in Messages
104 | Messages.update({_id: messageId}, {$set: {conversation: conversation}});
105 | },
106 | 'messages.addMessage': (messageId, toUserId, body) => {
107 | check(messageId, String);
108 | check(toUserId, String);
109 | check(body, String);
110 |
111 | // Verify that user is logged in
112 | if (!Meteor.user()) {
113 | throw new Meteor.Error(401, 'You need to be signed in to continue');
114 | }
115 |
116 | if (!body) {
117 | throw new Meteor.Error(422, 'Body should not be blank');
118 | }
119 |
120 | if (!toUserId) {
121 | throw new Meteor.Error(422, 'Error finding other user');
122 | }
123 |
124 | // Verify that message exists
125 | if (Messages.find({_id: messageId}).count() === 0) {
126 | throw new Meteor.Error(111, 'Not a valid message');
127 | }
128 |
129 | let newMessage = {
130 | from: {
131 | userId: Meteor.userId()
132 | },
133 | to: {
134 | userId: toUserId,
135 | read: false
136 | },
137 | body: body,
138 | sentOn: new Date(),
139 | originatingFromDeleted: false,
140 | originatingToDeleted: false
141 | };
142 |
143 | // Add item to array
144 | Messages.update({_id: messageId}, {$push: {conversation: newMessage}});
145 | }
146 | });
147 |
--------------------------------------------------------------------------------
/collections/posts.js:
--------------------------------------------------------------------------------
1 | Posts = new Mongo.Collection('posts');
2 |
3 | Meteor.methods({
4 | 'posts.insert': (body) => {
5 | check(body, String);
6 |
7 | if (!Meteor.user()) {
8 | throw new Meteor.Error(401, 'You need to be signed in to continue');
9 | }
10 | if (!body) {
11 | throw new Meteor.Error(422, 'Body should not be blank');
12 | }
13 |
14 | let post = {
15 | body: body,
16 | authorId: Meteor.userId(),
17 | createdAt: new Date(),
18 | updatedAt: new Date(),
19 | likecount: 0,
20 | already_voted: []
21 | };
22 |
23 | return Posts.insert(post);
24 | },
25 | 'posts.remove': (_id) => {
26 | check(_id, String);
27 |
28 | if (!Meteor.user()) {
29 | throw new Meteor.Error(401, 'You need to be signed in to continue');
30 | }
31 | if (!_id) {
32 | throw new Meteor.Error(422, '_id should not be blank');
33 | }
34 | if (Meteor.userId() !== Posts.findOne({ _id: _id }).authorId) {
35 | throw new Meteor.Error(422, 'You can only remove your own posts');
36 | }
37 |
38 | Posts.remove({ _id: _id });
39 | },
40 | 'posts.like': (_id) => {
41 | check(_id, String);
42 |
43 | if (!Meteor.user()) {
44 | throw new Meteor.Error(401, 'You need to be signed in to continue');
45 | }
46 | if (!_id) {
47 | throw new Meteor.Error(422, '_id should not be blank');
48 | }
49 |
50 | if (Posts.find( { _id: _id, already_voted: { $in: [Meteor.userId()]} }).count() === 0) {
51 | Posts.update( { _id: _id }, { $push: { already_voted: Meteor.userId() } });
52 | Posts.update({ _id: _id }, { $inc: {likecount: 1} });
53 | } else if (Posts.find( { _id: _id, already_voted: { $in: [Meteor.userId()]} }).count() === 1) {
54 | Posts.update( { _id: _id }, { $pull: { already_voted: Meteor.userId() } });
55 | Posts.update({ _id: _id }, { $inc: { likecount: -1} });
56 | }
57 | }
58 | });
59 |
--------------------------------------------------------------------------------
/collections/users.js:
--------------------------------------------------------------------------------
1 | Meteor.methods({
2 | 'users.updateProfile': (user) => {
3 | check(user, {
4 | biography: String,
5 | socialMedia: Object
6 | });
7 |
8 | if (!Meteor.user()) {
9 | throw new Meteor.Error(401, 'You need to be signed in to continue');
10 | }
11 |
12 | Meteor.users.update({ _id: Meteor.userId() }, { $set: { biography: user.biography, socialMedia: user.socialMedia } } );
13 | },
14 |
15 | 'users.follow': (_id) => {
16 | check(_id, String);
17 |
18 | if (!Meteor.user()) {
19 | throw new Meteor.Error(401, 'You need to be signed in to continue');
20 | }
21 | if (Meteor.userId() === _id) {
22 | throw new Meteor.Error(422, 'You can not follow yourself');
23 | }
24 |
25 | Meteor.users.update({ _id: Meteor.userId() }, { $addToSet: { followingIds: _id } });
26 | },
27 |
28 | 'users.unfollow': (_id) => {
29 | check(_id, String);
30 |
31 | if (!Meteor.user()) {
32 | throw new Meteor.Error(401, 'You need to be signed in to continue');
33 | }
34 | if (Meteor.userId() === _id) {
35 | throw new Meteor.Error(422, 'You can not unfollow yourself');
36 | }
37 |
38 | Meteor.users.update({ _id: Meteor.userId() }, { $pull: { followingIds: _id } });
39 | }
40 | });
41 |
--------------------------------------------------------------------------------
/lib/accounts/config.js:
--------------------------------------------------------------------------------
1 | AccountsTemplates.configure({
2 | defaultLayout: 'layout',
3 | defaultLayoutRegions: {
4 | header: 'header'// ,
5 | // footer: '_footer'
6 | },
7 | defaultContentRegion: 'main',
8 | showForgotPasswordLink: true,
9 | overrideLoginErrors: true,
10 | enablePasswordChange: true,
11 | sendVerificationEmail: true,
12 | lowercaseUsername: false,
13 |
14 | // enforceEmailVerification: true,
15 | confirmPassword: true,
16 | continuousValidation: true,
17 | // displayFormLabels: true,
18 | // forbidClientAccountCreation: false,
19 | homeRoutePath: '/',
20 | // showAddRemoveServices: false,
21 | showPlaceholders: true,
22 |
23 | negativeValidation: true,
24 | positiveValidation: true,
25 | negativeFeedback: false,
26 | positiveFeedback: false,
27 |
28 | // privacyUrl: 'privacy',
29 | // termsUrl: 'terms',
30 |
31 | texts: {
32 | title: {
33 | changePwd: 'Change password',
34 | forgotPwd: 'Forgot password?',
35 | resetPwd: 'Reset password',
36 | signIn: 'Sign in',
37 | signUp: 'Sign up',
38 | verifyEmail: 'Verify Email'
39 | },
40 | button: {
41 | changePwd: 'Change password',
42 | enrollAccount: 'Enroll Text',
43 | forgotPwd: 'Send reset link',
44 | resetPwd: 'Reset Password',
45 | signIn: 'Sign in',
46 | signUp: 'Sign up'
47 | }
48 | }
49 | });
50 |
51 | // configuring useraccounts for login with both username or email
52 | let pwd = AccountsTemplates.removeField('password');
53 | AccountsTemplates.removeField('email');
54 | AccountsTemplates.addFields([
55 | {
56 | _id: 'email',
57 | type: 'email',
58 | required: true,
59 | displayName: 'Email',
60 | re: /.+@(.+){2,}\.(.+){2,}/,
61 | errStr: 'Invalid email'
62 | },
63 | {
64 | _id: 'username',
65 | type: 'text',
66 | displayName: 'Username',
67 | required: true,
68 | minLength: 4
69 | },
70 | {
71 | _id: 'username_and_email',
72 | placeholder: 'Username or email',
73 | type: 'text',
74 | required: true,
75 | displayName: 'Login'
76 | },
77 | pwd
78 | ]);
79 |
80 | // enable preconfigured Flow-Router routes by useraccounts:flow-router.
81 | AccountsTemplates.configureRoute('changePwd');
82 | AccountsTemplates.configureRoute('forgotPwd');
83 | AccountsTemplates.configureRoute('resetPwd');
84 | AccountsTemplates.configureRoute('signIn');
85 | AccountsTemplates.configureRoute('signUp');
86 | AccountsTemplates.configureRoute('verifyEmail');
87 | // AccountsTemplates.configureRoute('enrollAccount'); // for creating passwords after logging first time
88 |
--------------------------------------------------------------------------------
/lib/avatars/config.js:
--------------------------------------------------------------------------------
1 | Avatar.setOptions({
2 | imageSizes: {
3 | 'extra-large': 200,
4 | 'extra-small': 20
5 | }
6 | });
7 |
--------------------------------------------------------------------------------
/lib/router.js:
--------------------------------------------------------------------------------
1 | publicAccessible = FlowRouter.group({});
2 |
3 | signInRequired = FlowRouter.group({
4 | triggersEnter: [AccountsTemplates.ensureSignedIn]
5 | });
6 |
7 | signInRequired.route('/', {
8 | name: 'feed',
9 | action: () => {
10 | BlazeLayout.render('layout', {
11 | main: 'feed'
12 | });
13 | setTitle('Feed');
14 | }
15 | });
16 |
17 | signInRequired.route('/update-profile', {
18 | name: 'updateProfile',
19 | action: () => {
20 | BlazeLayout.render('layout', {
21 | main: 'updateProfile'
22 | });
23 | setTitle('Update profile');
24 | }
25 | });
26 |
27 | signInRequired.route('/users/:_id', {
28 | name: 'profile',
29 | action: () => {
30 | BlazeLayout.render('layout', {
31 | main: 'profile'
32 | });
33 | setTitle('Profile');
34 | }
35 | });
36 |
37 | signInRequired.route('/browse-users', {
38 | name: 'browseUsers',
39 | action: () => {
40 | BlazeLayout.render('layout', {
41 | main: 'browseUsers'
42 | });
43 | setTitle('Browse users');
44 | }
45 | });
46 |
47 | signInRequired.route('/following', {
48 | name: 'following',
49 | action: () => {
50 | BlazeLayout.render('layout', {
51 | main: 'following'
52 | });
53 | setTitle('Following');
54 | }
55 | });
56 |
57 | signInRequired.route('/follower', {
58 | name: 'follower',
59 | action: () => {
60 | BlazeLayout.render('layout', {
61 | main: 'follower'
62 | });
63 | setTitle('Follower');
64 | }
65 | });
66 |
67 | signInRequired.route('/messages', {
68 | name: 'messages',
69 | action: () => {
70 | BlazeLayout.render('layout', {
71 | main: 'messages'
72 | });
73 | setTitle('Messages');
74 | }
75 | });
76 |
77 | signInRequired.route('/jobBoard', {
78 | name: 'jobBoard',
79 | action: () => {
80 | BlazeLayout.render('layout', {
81 | main: 'jobBoard'
82 | });
83 | setTitle('Job board');
84 | }
85 | });
86 |
--------------------------------------------------------------------------------
/license.md:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2016 Gravity
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/public/img/logo-gravity.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GravityProject/gravity/07d5295d7e7ef3cff9d4cc289abc10328fe80dda/public/img/logo-gravity.png
--------------------------------------------------------------------------------
/screenshot-1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GravityProject/gravity/07d5295d7e7ef3cff9d4cc289abc10328fe80dda/screenshot-1.png
--------------------------------------------------------------------------------
/server/indexes.js:
--------------------------------------------------------------------------------
1 | Posts._ensureIndex({
2 | body: 'text'
3 | });
4 |
5 | Meteor.users._ensureIndex({
6 | username: 'text'
7 | });
8 |
--------------------------------------------------------------------------------
/server/publications.js:
--------------------------------------------------------------------------------
1 | Meteor.publish(null, function() {
2 | if (this.userId) {
3 | return Meteor.users.find({ _id: this.userId }, { fields: { biography: 1, followingIds: 1 } });
4 | }
5 | });
6 |
7 | Meteor.publish('userStatus', function() {
8 | return Meteor.users.find({'status.online': true });
9 | });
10 |
11 | Meteor.publishComposite('posts.all', function(query, filter, limit) {
12 | check(query, String);
13 | check(filter, String);
14 | check(limit, Number);
15 |
16 | if (this.userId) {
17 | let currentUser = Meteor.users.findOne({ _id: this.userId });
18 |
19 | let parameters = {
20 | find: {},
21 | options: {}
22 | };
23 |
24 | if (filter === 'following') {
25 | if (currentUser.followingIds && currentUser.followingIds.length !== 0) {
26 | parameters.find.authorId = { $in: currentUser.followingIds };
27 | } else {
28 | parameters.find.authorId = { $in: [] };
29 | }
30 | }
31 |
32 | return {
33 | find: () => {
34 | if (query) {
35 | parameters.find.$text = { $search: query };
36 | parameters.options = {
37 | fields: { score: { $meta: 'textScore' } },
38 | sort: { score: { $meta: 'textScore' } },
39 | limit: limit
40 | };
41 | } else {
42 | parameters.options = { sort: { createdAt: -1 }, limit: limit };
43 | }
44 | Counts.publish(this, 'posts.all', Posts.find(parameters.find), { noReady: true });
45 | return Posts.find(parameters.find, parameters.options);
46 | },
47 | children: [
48 | {
49 | find: (post) => {
50 | return Meteor.users.find({ _id: post.authorId }, { fields: { emails: 1, username: 1 } });
51 | }
52 | }
53 | ]
54 | };
55 | } else {
56 | return [];
57 | }
58 | });
59 |
60 | Meteor.publishComposite('users.profile', function(_id, limit) {
61 | check(_id, String);
62 | check(limit, Number);
63 |
64 | if (this.userId) {
65 | return {
66 | find: () => {
67 | return Meteor.users.find({ _id: _id });
68 | },
69 | children: [
70 | {
71 | find: (user) => {
72 | Counts.publish(this, 'users.profile', Posts.find({ authorId: user._id }), { noReady: true });
73 | return Posts.find({ authorId: user._id }, { sort: { createdAt: -1 }, limit: limit });
74 | }
75 | }
76 | ]
77 | };
78 | } else {
79 | return [];
80 | }
81 | });
82 |
83 | Meteor.publish('users.all', function(query, limit) {
84 | check(query, String);
85 | check(limit, Number);
86 |
87 | if (this.userId) {
88 | if (query) {
89 | Counts.publish(this, 'users.all', Meteor.users.find({ $text: { $search: query } }), { noReady: true });
90 | return Meteor.users.find(
91 | {
92 | $text: {
93 | $search: query
94 | }
95 | },
96 | {
97 | fields: {
98 | score: {
99 | $meta: 'textScore'
100 | }
101 | },
102 | sort: {
103 | score: {
104 | $meta: 'textScore'
105 | }
106 | },
107 | limit: limit
108 | }
109 | );
110 | } else {
111 | Counts.publish(this, 'users.all', Meteor.users.find(), { noReady: true });
112 | return Meteor.users.find({}, { sort: { createdAt: -1 }, limit: limit });
113 | }
114 | } else {
115 | return [];
116 | }
117 | });
118 |
119 | Meteor.publish('users.following', function() {
120 | if (this.userId) {
121 | let currentUser = Meteor.users.findOne({ _id: this.userId });
122 |
123 | if (currentUser.followingIds && currentUser.followingIds.length !== 0) {
124 | return Meteor.users.find({ _id: { $in: currentUser.followingIds } }, { sort: { username: 1 } });
125 | } else {
126 | return [];
127 | }
128 | } else {
129 | return [];
130 | }
131 | });
132 |
133 | Meteor.publish('users.follower', function() {
134 | if (this.userId) {
135 | let currentUser = Meteor.users.findOne({ _id: this.userId });
136 |
137 | return Meteor.users.find({ followingIds: { $in: [currentUser._id] } }, { sort: { username: 1 } });
138 | } else {
139 | return [];
140 | }
141 | });
142 |
143 | Meteor.publish('messages.all', function() {
144 | if (this.userId) {
145 | let currentUser = Meteor.users.findOne({_id: this.userId});
146 |
147 | return Messages.find({ $or: [{ originatingFromId: currentUser._id }, {originatingToId: currentUser._id }] });
148 | } else {
149 | return [];
150 | }
151 | });
152 |
153 | Meteor.publish('jobs.all', function(query, limit) {
154 | check(query, String);
155 | check(limit, Number);
156 |
157 | if (this.userId) {
158 | if (query) {
159 | Counts.publish(this, 'jobs.all', Jobs.find({title: { $regex: '.*' + query + '.*', $options: 'i' } }), { noReady: true });
160 | return Jobs.find({ title: { $regex: '.*' + query + '.*', $options: 'i' } }, { sort: { createdOn: -1 }, limit: limit });
161 | } else {
162 | Counts.publish(this, 'jobs.all', Jobs.find({}), { noReady: true });
163 | return Jobs.find({}, { sort: { createdOn: -1 }, limit: limit });
164 | }
165 | } else {
166 | return [];
167 | }
168 | });
169 |
--------------------------------------------------------------------------------
/server/startup.js:
--------------------------------------------------------------------------------
1 | Meteor.startup(() => {
2 | Accounts.validateNewUser((user) => {
3 | if (user.emails && user.emails[0].address.length !== 0) {
4 | return true;
5 | }
6 | throw new Meteor.Error(403, 'E-Mail address should not be blank');
7 | });
8 | Accounts.validateNewUser((user) => {
9 | if (user.username && user.username.length >= 3) {
10 | return true;
11 | }
12 | throw new Meteor.Error(403, 'Username must have at least 3 characters');
13 | });
14 | });
15 |
--------------------------------------------------------------------------------