diff --git a/app/components/comments/comment-compose.js b/app/components/comments/comment-compose.js new file mode 100644 index 0000000..5f83d29 --- /dev/null +++ b/app/components/comments/comment-compose.js @@ -0,0 +1,50 @@ +import Component from 'ember-component'; +import get, { getProperties } from 'ember-metal/get'; +import set from 'ember-metal/set'; +import computed from 'ember-computed'; +import service from 'ember-service/inject'; +import { invokeAction } from 'ember-invoke-action'; +import { next } from 'ember-runloop'; +import { isEmpty } from 'ember-utils'; +import $ from 'jquery'; + +export default Component.extend({ + classNames: ['comment-compose'], + expanded: false, + store: service(), + + editing: computed('text', function() { + return !isEmpty(get(this, 'text')); + }).readOnly(), + + reset() { + set(this, 'text', null); + set(this, 'expanded', false); + }, + + actions: { + expand() { + set(this, 'expanded', true); + next(() => { + $('.comment-compose-link').focus(); + }); + }, + + collapse() { + if (!get(this, 'editing')) { + set(this, 'expanded', false); + } + }, + + create() { + const store = get(this, 'store'); + const { text, post } = getProperties(this, 'text', 'post'); + const user = get(this, 'session.account'); + const comment = store.createRecord('comment', { text, user, post }); + comment.save().then((createdComment) => { + invokeAction(this, 'insertComment', createdComment); + this.reset(); + }); + } + } +}); diff --git a/app/components/comments/comment-edit.js b/app/components/comments/comment-edit.js new file mode 100644 index 0000000..10d10ea --- /dev/null +++ b/app/components/comments/comment-edit.js @@ -0,0 +1,19 @@ +import Component from 'ember-component'; +import set from 'ember-metal/set'; + +export default Component.extend({ + classNames: ['comment-edit'], + + actions: { + save(comment) { + comment.save().then(() => set(this, 'editing', false)); + }, + + cancel(comment) { + // I don't think ember-change-set is needed here + comment.rollbackAttributes(); + set(this, 'editing', false); + } + } + +}); diff --git a/app/components/comments/comment-view.js b/app/components/comments/comment-view.js new file mode 100644 index 0000000..ea44b3c --- /dev/null +++ b/app/components/comments/comment-view.js @@ -0,0 +1,20 @@ +import Component from 'ember-component'; +import get from 'ember-metal/get'; +import computed from 'ember-computed'; + +export default Component.extend({ + classNames: ['comment-view'], + editing: false, + + owner: computed('session', 'comment', function() { + return get(this, 'session.account.id') == get(this, 'comment.user.id'); + }).readOnly(), + + actions: { + delete_comment(comment) { + // add confirmation popup? + comment.destroyRecord(); + } + } + +}); diff --git a/app/components/comments/comments-list.js b/app/components/comments/comments-list.js new file mode 100644 index 0000000..1695228 --- /dev/null +++ b/app/components/comments/comments-list.js @@ -0,0 +1,38 @@ +import Component from 'ember-component'; +import service from 'ember-service/inject'; +import get from 'ember-metal/get'; +import set from 'ember-metal/set'; +import { task } from 'ember-concurrency'; + +export default Component.extend({ + classNames: ['comment-list'], + expanded: false, + store: service(), + + init() { + this._super(...arguments); + get(this, 'getComments').perform(); + }, + + actions: { + expand() { + set(this, 'expanded', true); + }, + + collapse() { + set(this, 'expanded', false); + }, + insertComment(comment) { + get(this, 'comments').pushObject(comment._internalModel); + } + }, + + getComments: task(function* () { + const comments = yield get(this, 'store').query('comment', { + filter: { postId: get(this, 'post.id') }, + include: 'user', + sort: 'createdAt' + }); + set(this, 'comments', comments); + }) +}); diff --git a/app/components/friendships/friendship-entry.js b/app/components/friendships/friendship-entry.js new file mode 100644 index 0000000..aeb7f3d --- /dev/null +++ b/app/components/friendships/friendship-entry.js @@ -0,0 +1,19 @@ +import Component from 'ember-component'; +import service from 'ember-service/inject'; + +export default Component.extend({ + classNames: ['friendship-entry'], + store: service(), + + actions: { + accept(friendship) { + friendship.set('confirmed', true); + friendship.save(); + }, + + // TODO: save decline status instead of deleting record + remove(friendship) { + friendship.destroyRecord(); + } + } +}); diff --git a/app/components/posts/post-edit.js b/app/components/posts/post-edit.js new file mode 100644 index 0000000..027da39 --- /dev/null +++ b/app/components/posts/post-edit.js @@ -0,0 +1,19 @@ +import Component from 'ember-component'; +import set from 'ember-metal/set'; + +export default Component.extend({ + classNames: ['post-edit'], + + actions: { + save(post) { + post.save().then(() => set(this, 'editing', false)); + }, + + cancel(post) { + // I don't think ember-change-set is needed here + post.rollbackAttributes(); + set(this, 'editing', false); + } + } + +}); diff --git a/app/components/posts/post-view.js b/app/components/posts/post-view.js index 83b59b7..7c8b7d3 100644 --- a/app/components/posts/post-view.js +++ b/app/components/posts/post-view.js @@ -1,5 +1,20 @@ import Component from 'ember-component'; +import get from 'ember-metal/get'; +import computed from 'ember-computed'; export default Component.extend({ - classNames: ['post-view'] + classNames: ['post-view'], + editing: false, + + owner: computed('session', 'post', function() { + return get(this, 'session.account.id') == get(this, 'post.user.id'); + }).readOnly(), + + actions: { + delete_post(post) { + // add confirmation popup? + post.destroyRecord(); + } + } + }); diff --git a/app/components/users/user-entry.js b/app/components/users/user-entry.js new file mode 100644 index 0000000..7f4dc73 --- /dev/null +++ b/app/components/users/user-entry.js @@ -0,0 +1,21 @@ +import Component from 'ember-component'; +import get from 'ember-metal/get'; +import service from 'ember-service/inject'; + +export default Component.extend({ + classNames: ['user-entry'], + store: service(), + + actions: { + befriend(friend) { + const user = get(this, 'session.account'); + const store = get(this, 'store'); + const friendship = store.createRecord('friendship', { + user: user, + friend: friend, + confirmed: false + }); + friendship.save(); + } + } +}); diff --git a/app/models/comment.js b/app/models/comment.js new file mode 100644 index 0000000..5956bc9 --- /dev/null +++ b/app/models/comment.js @@ -0,0 +1,12 @@ +import Model from 'ember-data/model'; +import attr from 'ember-data/attr'; +import { belongsTo } from 'ember-data/relationships'; + +export default Model.extend({ + text: attr('string'), + rating: attr('integer'), + createdAt: attr('utc'), + updatedAt: attr('utc'), + user: belongsTo('user'), + post: belongsTo('post') +}); diff --git a/app/models/friendship.js b/app/models/friendship.js new file mode 100644 index 0000000..dafba08 --- /dev/null +++ b/app/models/friendship.js @@ -0,0 +1,10 @@ +import Model from 'ember-data/model'; +import attr from 'ember-data/attr'; +import { belongsTo } from 'ember-data/relationships'; + +export default Model.extend({ + createdAt: attr('utc'), + confirmed: attr('boolean'), + user: belongsTo('user', { inverse: 'sent_friendships' }), + friend: belongsTo('user', { inverse: 'received_friendships' }) +}); diff --git a/app/models/user.js b/app/models/user.js index 6250458..cad162c 100644 --- a/app/models/user.js +++ b/app/models/user.js @@ -39,5 +39,16 @@ export default Model.extend(Validations, { email: attr('string'), password: attr('string'), posts: hasMany('post'), - subscriptions: hasMany('subscription') + subscriptions: hasMany('subscription'), + friendships: hasMany('friendship'), + friends: hasMany('user', { + inverse: null}), + confirmed_friends: hasMany('user', { + inverse: null}), + // not in use but don't know how to remove without breaking ember + sent_friendships: hasMany('friendship', { + inverse: 'user'}), + // not in use but don't know how to remove without breaking ember + received_friendships: hasMany('friendship', { + inverse: 'friend'}) }); diff --git a/app/router.js b/app/router.js index 0dba0fa..af0b9c1 100644 --- a/app/router.js +++ b/app/router.js @@ -20,9 +20,10 @@ Router.map(function() { this.route('new'); }); - this.route('users', { path: '/users/:name' }); + this.route('users', { path: '/users/:id' }); this.route('404', { path: '*' }); + this.route('search'); }); export default Router; diff --git a/app/routes/search.js b/app/routes/search.js new file mode 100644 index 0000000..b0a0c25 --- /dev/null +++ b/app/routes/search.js @@ -0,0 +1,45 @@ +import Ember from 'ember'; +import Route from 'ember-route'; +import get from 'ember-metal/get'; + +export default Route.extend({ + model() { + const users = get(this, 'store').query('user', { + filter: { + available_users_to_friend: true + } + }); + + const pending_friends = get(this, 'store').query('friendship', { + filter: { + confirmed_friends: false + } + }); + + const sent = get(this, 'store').query('friendship', { + filter: { + sent: true + } + }); + + const received = get(this, 'store').query('friendship', { + filter: { + received: true + } + }); + + const friends = get(this, 'store').query('friendship', { + filter: { + confirmed_friends: true + } + }); + + return Ember.RSVP.hash({ + users: users, + sent: sent, + received: received, + friends: friends, + pending_friends: pending_friends + }); + } +}); diff --git a/app/routes/users.js b/app/routes/users.js index 5d29eed..d98f570 100644 --- a/app/routes/users.js +++ b/app/routes/users.js @@ -3,11 +3,11 @@ import get from 'ember-metal/get'; export default Route.extend({ model(params) { - const { name } = params; + const { id } = params; return get(this, 'store') .query('user', { - filter: { name } + filter: { id } }) - .then(records => get(records, 'firstObject')) + .then(records => get(records, 'firstObject')); } }); diff --git a/app/styles/app.scss b/app/styles/app.scss index 93ceea8..7a04e20 100644 --- a/app/styles/app.scss +++ b/app/styles/app.scss @@ -80,5 +80,6 @@ a { @import 'channels'; @import 'dashboard'; @import 'posts'; +@import 'comments'; @import 'sign-up'; @import 'ui'; diff --git a/app/styles/comments.scss b/app/styles/comments.scss new file mode 100644 index 0000000..f1e3328 --- /dev/null +++ b/app/styles/comments.scss @@ -0,0 +1,99 @@ +.comment-compose, .comment-edit { + width: 100%; + max-width: 512px; + margin: 16px auto; + padding: 16px; + border-radius: 8px; + background: $darker-gray; + overflow: auto; + .comment-compose-header { + display: flex; + align-items: center; + width: 100%; + height: 16px; + cursor: text; + } + input { + height: 36px; + border-bottom: 1px solid $dim; + } + input, textarea { + width: 100%; + padding: 8px; + border-top: 0; + border-right: 0; + border-left: 0; + background: inherit; + color: $text; + font-size: medium; + resize: none; + } + textarea { + min-height: 128px; + margin-bottom: 8px; + border-bottom: 0; + overflow: hidden; + } + button { + padding: 8px 12px; + float: right; + border: 0; + border-radius: 4px; + background: $accent; + color: $darker-gray; + font-size: medium; + cursor: pointer; + &:disabled { + background: $text; + color: $dim; + cursor: default; + } + } +} + +.comment-list { + .comment-list-header { + background: $darkest-gray; + border-radius: 8px; + margin: 16px auto; + padding: 16px; + } +} +.comment-view { + max-width: 512px; + margin: 16px auto; + padding: 16px; + border-radius: 8px; + background: $darker-gray; + .comment-header { + text-align: right; + } + .comment-subject { + iframe { + width: 100%; + } + } + .comment-user { + display: flex; + align-items: center; + margin-top: 4px; + .comment-user-avatar { + width: 38px; + margin-right: 16px; + border-radius: 999em; + } + .comment-user-info { + .comment-user-name { + margin-bottom: 4px; + color: $bright; + font-size: 16px; + } + .timestamp { + font-size: 12px; + } + } + } + .comment-text { + margin-top: 8px; + } +} diff --git a/app/styles/posts.scss b/app/styles/posts.scss index d73b172..0c52710 100644 --- a/app/styles/posts.scss +++ b/app/styles/posts.scss @@ -1,4 +1,4 @@ -.post-compose { +.post-compose, .post-edit { width: 100%; max-width: 512px; margin: 16px auto; @@ -57,6 +57,9 @@ padding: 16px; border-radius: 8px; background: $dark-gray; + .post-header { + text-align: right; + } .post-subject { iframe { width: 100%; diff --git a/app/templates/application.hbs b/app/templates/application.hbs index e3d3f96..a0fa084 100644 --- a/app/templates/application.hbs +++ b/app/templates/application.hbs @@ -2,8 +2,12 @@ -{{#if post.text}} -
{{post.text}}
+{{#if (not editing)}} + {{#if post.text}} +
{{post.text}}
+ {{/if}} + {{comments/comments-list post=post}} +{{else}} + {{posts/post-edit post=post editing=editing}} {{/if}} diff --git a/app/templates/components/users/user-entry.hbs b/app/templates/components/users/user-entry.hbs new file mode 100644 index 0000000..fbc893e --- /dev/null +++ b/app/templates/components/users/user-entry.hbs @@ -0,0 +1,8 @@ +
+ {{user.first_name}} {{user.last_name}} + +
+ + diff --git a/app/templates/search.hbs b/app/templates/search.hbs new file mode 100644 index 0000000..5913341 --- /dev/null +++ b/app/templates/search.hbs @@ -0,0 +1,32 @@ +
+
+ Users: +
+ {{#each model.users as |user|}} + {{users/user-entry user=user}} + {{/each}} + +
+ pending friends: + {{#each model.pending_friends as |friendship|}} + {{friendships/friendship-entry friendship=friendship}} + {{/each}} + +
+ sent: + {{#each model.sent as |friendship|}} + {{friendships/friendship-entry friendship=friendship}} + {{/each}} + +
+ received: + {{#each model.received as |friendship|}} + {{friendships/friendship-entry friendship=friendship}} + {{/each}} + +
+ friends: + {{#each model.friends as |friendship|}} + {{friendships/friendship-entry friendship=friendship}} + {{/each}} +
diff --git a/app/templates/users.hbs b/app/templates/users.hbs index 1c25739..e74ebc4 100644 --- a/app/templates/users.hbs +++ b/app/templates/users.hbs @@ -1,4 +1,14 @@ {{#with model as |user|}} - {{user.name}} +
+ Name: {{user.first_name}} {{user.last_name}} +
+ Email: {{user.email}} +
+
+ Friends: + {{#each user.confirmed_friends as |friend|}} + Name: {{friend.first_name}} +
+ {{/each}} +
{{/with}} -{{outlet}}