From 293ce75096fb373dd0355d253252aeeec1c3e112 Mon Sep 17 00:00:00 2001 From: Josh Smith Date: Wed, 17 Feb 2016 22:31:37 -0800 Subject: [PATCH] Add navigation UI --- app/adapters/current-user.js | 30 ++++ app/components/slugged-route-model-details.js | 4 +- app/components/user-menu.js | 22 +++ app/index.html | 3 + app/models/current-user.js | 3 + app/models/organization.js | 4 +- app/models/{model.js => owner.js} | 0 app/models/slugged-route.js | 4 +- app/models/user.js | 14 +- app/services/session.js | 17 +++ app/styles/_fonts.scss | 3 +- app/styles/_header.scss | 11 ++ app/styles/_misc.scss | 40 ++++++ app/styles/app.scss | 11 +- app/styles/components/user-details.scss | 48 +++++++ app/styles/components/user-menu.scss | 128 ++++++++++++++++++ app/styles/main.scss | 8 +- app/templates/application.hbs | 24 +++- .../slugged-route-model-details.hbs | 10 +- app/templates/components/user-details.hbs | 17 ++- app/templates/components/user-menu.hbs | 20 +++ config/environment.js | 4 + mirage/config.js | 11 ++ mirage/models/slugged-route.js | 32 ++--- package.json | 2 + public/assets/images/icons/cc-logo-small.png | Bin 0 -> 1966 bytes .../assets/images/icons/cc-logo-small@2x.png | Bin 0 -> 4273 bytes public/assets/images/icons/cc-logo.png | Bin 0 -> 6615 bytes public/assets/images/icons/cc-logo@2x.png | Bin 0 -> 14336 bytes public/assets/images/icons/link.png | Bin 0 -> 573 bytes public/assets/images/icons/link@2x.png | Bin 0 -> 1194 bytes public/assets/images/icons/location.png | Bin 0 -> 484 bytes public/assets/images/icons/location@2x.png | Bin 0 -> 944 bytes public/assets/images/icons/twitter.png | Bin 0 -> 456 bytes public/assets/images/icons/twitter@2x.png | Bin 0 -> 928 bytes tests/acceptance/login-test.js | 2 +- tests/acceptance/logout-test.js | 9 +- tests/acceptance/navigation-test.js | 100 ++++++++++++++ tests/acceptance/post-test.js | 42 ------ tests/acceptance/projects-test.js | 2 +- tests/acceptance/signup-test.js | 16 +-- tests/acceptance/slugged-route-test.js | 10 +- .../components/error-wrapper-test.js | 16 +-- .../slugged-route-model-details-test.js | 4 +- .../integration/components/user-menu-test.js | 46 +++++++ .../models/{model-test.js => owner-test.js} | 2 +- tests/unit/models/slugged-route-test.js | 2 +- tests/unit/models/user-test.js | 2 +- 48 files changed, 603 insertions(+), 120 deletions(-) create mode 100644 app/adapters/current-user.js create mode 100644 app/components/user-menu.js create mode 100644 app/models/current-user.js rename app/models/{model.js => owner.js} (100%) create mode 100644 app/services/session.js create mode 100644 app/styles/_header.scss create mode 100644 app/styles/_misc.scss create mode 100644 app/styles/components/user-details.scss create mode 100644 app/styles/components/user-menu.scss create mode 100644 app/templates/components/user-menu.hbs create mode 100644 public/assets/images/icons/cc-logo-small.png create mode 100644 public/assets/images/icons/cc-logo-small@2x.png create mode 100644 public/assets/images/icons/cc-logo.png create mode 100644 public/assets/images/icons/cc-logo@2x.png create mode 100644 public/assets/images/icons/link.png create mode 100644 public/assets/images/icons/link@2x.png create mode 100644 public/assets/images/icons/location.png create mode 100644 public/assets/images/icons/location@2x.png create mode 100644 public/assets/images/icons/twitter.png create mode 100644 public/assets/images/icons/twitter@2x.png create mode 100644 tests/acceptance/navigation-test.js delete mode 100644 tests/acceptance/post-test.js create mode 100644 tests/integration/components/user-menu-test.js rename tests/unit/models/{model-test.js => owner-test.js} (83%) diff --git a/app/adapters/current-user.js b/app/adapters/current-user.js new file mode 100644 index 000000000..0f83c4c55 --- /dev/null +++ b/app/adapters/current-user.js @@ -0,0 +1,30 @@ +import Ember from 'ember'; +import ApplicationAdapter from './application'; + +export default ApplicationAdapter.extend({ + session: Ember.inject.service(), + + buildURL(modelName, id, snapshot, requestType) { + if (requestType === 'findRecord') { + if (parseInt(id) === parseInt(this.get('session.session.authenticated.user_id'))) { + return this.urlForCurrentUser(); + } + } + return this._super(...arguments); + }, + + urlForCurrentUser() { + var url = []; + var host = Ember.get(this, 'host'); + var prefix = this.urlPrefix(); + + url.push(encodeURIComponent('user')); + + if (prefix) { url.unshift(prefix); } + + url = url.join('/'); + if (!host && url) { url = '/' + url; } + + return url; + } +}); diff --git a/app/components/slugged-route-model-details.js b/app/components/slugged-route-model-details.js index 8d0a4fb51..383ba337f 100644 --- a/app/components/slugged-route-model-details.js +++ b/app/components/slugged-route-model-details.js @@ -3,6 +3,6 @@ import Ember from 'ember'; export default Ember.Component.extend({ classNames: ['slugged-route-model-details'], - isOrganization: Ember.computed.equal('sluggedRoute.modelType', 'organization'), - isUser: Ember.computed.equal('sluggedRoute.modelType', 'user') + belongsToOrganization: Ember.computed.equal('sluggedRoute.ownerType', 'Organization'), + belongsToUser: Ember.computed.equal('sluggedRoute.ownerType', 'User'), }); diff --git a/app/components/user-menu.js b/app/components/user-menu.js new file mode 100644 index 000000000..d3831581f --- /dev/null +++ b/app/components/user-menu.js @@ -0,0 +1,22 @@ +import Ember from 'ember'; + +export default Ember.Component.extend({ + classNames: ['user-menu'], + classNameBindings: ['menuHidden:menu-hidden:menu-visible'], + + session: Ember.inject.service(), + + menuHidden: true, + + actions: { + toggle: function() { + this.toggleProperty('menuHidden'); + }, + hide: function() { + this.set('menuHidden', true); + }, + invalidateSession: function() { + this.get('session').invalidate(); + } + } +}); diff --git a/app/index.html b/app/index.html index 0c522ae25..9b3582d56 100644 --- a/app/index.html +++ b/app/index.html @@ -7,6 +7,9 @@ + + + {{content-for 'head'}} diff --git a/app/models/current-user.js b/app/models/current-user.js new file mode 100644 index 000000000..88e5b5eec --- /dev/null +++ b/app/models/current-user.js @@ -0,0 +1,3 @@ +import User from 'code-corps-ember/models/user'; + +export default User.extend(); diff --git a/app/models/organization.js b/app/models/organization.js index 27a301554..08ad27390 100644 --- a/app/models/organization.js +++ b/app/models/organization.js @@ -1,9 +1,9 @@ import DS from 'ember-data'; -import Model from 'code-corps-ember/models/model'; +import Owner from 'code-corps-ember/models/owner'; var attr = DS.attr; -export default Model.extend({ +export default Owner.extend({ name: attr(), slug: attr(), }); diff --git a/app/models/model.js b/app/models/owner.js similarity index 100% rename from app/models/model.js rename to app/models/owner.js diff --git a/app/models/slugged-route.js b/app/models/slugged-route.js index faf3a94e4..ec1cc0c0b 100644 --- a/app/models/slugged-route.js +++ b/app/models/slugged-route.js @@ -1,7 +1,7 @@ import DS from 'ember-data'; export default DS.Model.extend({ - model: DS.belongsTo('model', { async: true, polymorphic: true }), - modelType: DS.attr('string'), + owner: DS.belongsTo('owner', { async: true, polymorphic: true }), + ownerType: DS.attr('string'), slug: DS.attr('string'), }); diff --git a/app/models/user.js b/app/models/user.js index 287fa95d0..bb719d65f 100644 --- a/app/models/user.js +++ b/app/models/user.js @@ -1,7 +1,8 @@ import DS from 'ember-data'; -import Model from 'code-corps-ember/models/model'; +import Owner from 'code-corps-ember/models/owner'; +import Ember from 'ember'; -export default Model.extend({ +export default Owner.extend({ name: DS.attr('string'), username: DS.attr('string'), website: DS.attr('string'), @@ -9,5 +10,14 @@ export default Model.extend({ biography: DS.attr('string'), email: DS.attr('string'), password: DS.attr('string'), + photoThumbUrl: DS.attr('string'), + photoLargeUrl: DS.attr('string'), createdAt: DS.attr('date'), + organizations: DS.hasMany('organization', { async: true }), + atUsername: Ember.computed('username', function() { + return `@${this.get('username')}`; + }), + twitterUrl: Ember.computed('twitter', function() { + return `https://twitter.com/${this.get('twitter')}`; + }) }); diff --git a/app/services/session.js b/app/services/session.js new file mode 100644 index 000000000..8558e4c81 --- /dev/null +++ b/app/services/session.js @@ -0,0 +1,17 @@ +import Ember from 'ember'; +import ESASession from "ember-simple-auth/services/session"; + +export default ESASession.extend({ + + store: Ember.inject.service(), + + setCurrentUser: function() { + if (this.get('isAuthenticated')) { + let id = this.get('session.authenticated.user_id'); + this.get('store').findRecord('current-user', id).then((user) => { + this.set('currentUser', user); + }); + } + }.observes('isAuthenticated') + +}); diff --git a/app/styles/_fonts.scss b/app/styles/_fonts.scss index 1da844f8a..b009ceaf6 100644 --- a/app/styles/_fonts.scss +++ b/app/styles/_fonts.scss @@ -1 +1,2 @@ -@import url(https://fonts.googleapis.com/css?family=Open+Sans:400,700); +$header-font-family: "proxima-nova", sans-serif; +$body-font-family: "open-sans", sans-serif; diff --git a/app/styles/_header.scss b/app/styles/_header.scss new file mode 100644 index 000000000..18f6fa4d1 --- /dev/null +++ b/app/styles/_header.scss @@ -0,0 +1,11 @@ +.site-logo { + margin: 0; + line-height: 50px; +} + +.logo { + $file: "/assets/images/icons/cc-logo"; + @include background-image-retina($file, "png", 234px, 50px); + display: inline-block; + text-indent: -9999px; +} \ No newline at end of file diff --git a/app/styles/_misc.scss b/app/styles/_misc.scss new file mode 100644 index 000000000..9afc39af8 --- /dev/null +++ b/app/styles/_misc.scss @@ -0,0 +1,40 @@ +@mixin arrow-down { + content: ""; + display: inline-block; + width: 0; + height: 0; + vertical-align: middle; + border-left: 4px solid transparent; + border-right: 4px solid transparent; + border-top: 6px solid $dark-text; + border-bottom: none; +} + +@mixin arrow-up { + content: ""; + display: inline-block; + width: 0; + height: 0; + vertical-align: middle; + border-left: 4px solid transparent; + border-right: 4px solid transparent; + border-bottom: 6px solid $dark-text; + border-top: none; +} + +.css-truncate-target { + display: inline-block; + max-width: 125px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + vertical-align: top; +} + +.border-top { + border-top: 1px solid; +} + +.border-gray-light { + border-color: $border-light; +} \ No newline at end of file diff --git a/app/styles/app.scss b/app/styles/app.scss index ccccca15c..903f143b2 100644 --- a/app/styles/app.scss +++ b/app/styles/app.scss @@ -10,16 +10,21 @@ @import "inputs"; @import "forms"; @import "alerts"; +@import "misc"; @import "main"; +@import "header"; + +@import "components/editor-with-preview"; @import "components/error-wrapper"; @import "components/login-form"; +@import "components/post-details"; @import "components/post-item"; +@import "components/post-title"; @import "components/project-details"; @import "components/project-menu"; @import "components/project-post-list"; @import "components/signup-form"; -@import "components/editor-with-preview"; -@import "components/post-title"; -@import "components/post-details"; +@import "components/user-details"; +@import "components/user-menu"; diff --git a/app/styles/components/user-details.scss b/app/styles/components/user-details.scss new file mode 100644 index 000000000..593ff0ccc --- /dev/null +++ b/app/styles/components/user-details.scss @@ -0,0 +1,48 @@ +@mixin detail-icon($type) { + &:before { + $file: "/assets/images/icons/" + $type; + @include background-image-retina($file, "png", 16px, 16px); + content: ""; + display: inline-block; + width: 16px; + height: 16px; + margin: 0px 5px -2px 0; + } +} + +.user-details { + @include span-columns(3); + img { + width: 100%; + border-radius: 4px; + } + + h2 { + margin: 10px 0; + } + + .username { + font-size: 16px; + font-weight: 500; + color: $light-text; + } + + .details { + margin: 10px 0; + padding: 10px 0; + font-size: 14px; + color: $light-text; + + li { + padding: 4px 0; + } + + .twitter { + @include detail-icon('twitter'); + } + + .website { + @include detail-icon('link'); + } + } +} \ No newline at end of file diff --git a/app/styles/components/user-menu.scss b/app/styles/components/user-menu.scss new file mode 100644 index 000000000..68a796fdc --- /dev/null +++ b/app/styles/components/user-menu.scss @@ -0,0 +1,128 @@ +.menu { + padding: 10px 0; + + ul { + list-style-type: none; + @include clearfix; + } + + a.active { + font-weight: 700; + } + + ul.header-nav { + > li { + display: inline-block; + float: left; + } + + > li > a, .user-menu-select { + font-weight: 700; + border-bottom: 5px solid transparent; + + &.active { + color: $link-color; + border-bottom-color: $dark-blue; + } + + &:hover { + color: $link-color; + border-bottom-color: $dark-blue; + transition: border-bottom-color ease 0.3s; + } + } + } +} + +.main-nav, .auth-nav { + font-family: $body-font-family; + font-style: normal; + font-size: 14px; + font-weight: 500; + padding: 0 30px; + + li a { + display: block; + padding: 15px 10px; + color: $dark-text; + } +} + +.user-menu { + height: 40px; + + .avatar { + width: 40px; + height: 40px; + border-radius: 20px; + line-height: 1; + vertical-align: middle; + } + + &.menu-hidden { + .user-dropdown { + display: none; + } + + .dropdown-arrow { + @include arrow-down; + } + } + + &.menu-visible { + .user-dropdown { + display: block; + } + + .dropdown-arrow { + @include arrow-up; + } + } + + a:hover { + .dropdown-arrow { + border-top-color: $link-hover-color; + border-bottom-color: $link-hover-color; + } + } +} + +.user-menu-select { + padding: 5px; + display: block; +} + +.user-dropdown { + position: absolute; + z-index: 99999; + top: 60px; + right: 0; + width: 200px; + background: white; + border: 1px solid $gray; + border-radius: 4px; + -webkit-box-shadow: 0px 10px 41px -16px rgba(0,0,0,0.75); + -moz-box-shadow: 0px 10px 41px -16px rgba(0,0,0,0.75); + box-shadow: 0px 10px 41px -16px rgba(0,0,0,0.75); + + li { + display: block; + font-size: 14px; + padding: 3px 0; + } +} + +.dropdown-body { + padding: 10px 20px; +} + +.dropdown-footer { + padding: 15px 20px; + background: $background-gray; + font-size: 13px; + color: $light-text; + + p { + margin: 0; + } +} \ No newline at end of file diff --git a/app/styles/main.scss b/app/styles/main.scss index b4e83c984..33a65faac 100644 --- a/app/styles/main.scss +++ b/app/styles/main.scss @@ -1,5 +1,5 @@ html { - font-family: "Open Sans", sans-serif; + font-family: $body-font-family; &.danger { background: $danger-color; @@ -47,4 +47,10 @@ a { .right { float: right; +} + +ul { + margin: 0; + padding: 0; + list-style-type: none; } \ No newline at end of file diff --git a/app/templates/application.hbs b/app/templates/application.hbs index f60cd9653..3f67bb6db 100644 --- a/app/templates/application.hbs +++ b/app/templates/application.hbs @@ -1,9 +1,25 @@
@@ -17,4 +33,4 @@
{{outlet}} -
\ No newline at end of file + diff --git a/app/templates/components/slugged-route-model-details.hbs b/app/templates/components/slugged-route-model-details.hbs index 81feb8b26..71ce0b6d7 100644 --- a/app/templates/components/slugged-route-model-details.hbs +++ b/app/templates/components/slugged-route-model-details.hbs @@ -1,8 +1,6 @@ -

Name: {{sluggedRoute.model.name}}

-

Username: {{sluggedRoute.model.username}}

-{{#if isOrganization}} - {{organization-details organization=sluggedRoute.model}} +{{#if belongsToOrganization}} + {{organization-details organization=sluggedRoute.owner}} {{/if}} -{{#if isUser}} - {{user-details user=sluggedRoute.model}} +{{#if belongsToUser}} + {{user-details user=sluggedRoute.owner}} {{/if}} \ No newline at end of file diff --git a/app/templates/components/user-details.hbs b/app/templates/components/user-details.hbs index 7538a43da..ce7eeac77 100644 --- a/app/templates/components/user-details.hbs +++ b/app/templates/components/user-details.hbs @@ -1 +1,16 @@ -User details \ No newline at end of file +{{#each user.organizations as |organization|}} + {{organization.name}} +{{/each}} + +

+
{{user.name}}
+
{{user.username}}
+

+ \ No newline at end of file diff --git a/app/templates/components/user-menu.hbs b/app/templates/components/user-menu.hbs new file mode 100644 index 000000000..72d4d8e3d --- /dev/null +++ b/app/templates/components/user-menu.hbs @@ -0,0 +1,20 @@ +{{#click-outside action=(action "hide") except-selector=".user-dropdown"}} + + {{model.atUsername}} + + +{{/click-outside}} +
+ + +
\ No newline at end of file diff --git a/config/environment.js b/config/environment.js index 3fdc307a8..4451c153f 100644 --- a/config/environment.js +++ b/config/environment.js @@ -18,6 +18,10 @@ module.exports = function(environment) { // when it is created }, + typekit: { + kitId: 'jkb2eqa' + }, + flashMessageDefaults: { // flash message defaults timeout: 2000, diff --git a/mirage/config.js b/mirage/config.js index eab68c389..1ceee23b6 100644 --- a/mirage/config.js +++ b/mirage/config.js @@ -30,6 +30,17 @@ export default function() { this.get('/users/:id'); this.get('/users'); + this.get('/user', (schema) => { + // due to the nature of how we fetch the current user, all we can do here is + // return one of the users available in the schema, or create a new one + let users = schema.user.all(); + if (users.length > 0) { + return users[0]; + } else { + return schema.user.create(); + } + }); + // POST /posts this.post('/posts', (schema, request) => { let requestBody = JSON.parse(request.requestBody); diff --git a/mirage/models/slugged-route.js b/mirage/models/slugged-route.js index a1b646e13..bade9cba3 100644 --- a/mirage/models/slugged-route.js +++ b/mirage/models/slugged-route.js @@ -4,31 +4,31 @@ let SluggedRouteModel = Model.extend({ user: belongsTo(), organization: belongsTo(), - createModel(attrs, type) { + createOwner(attrs, type) { if (!type) { throw new Error('Type is required'); } - this.modelType = type; + this.ownerType = type; - if (type === 'user') { + if (type === 'User') { return this.createUser(attrs); - } else if (type === 'organization') { + } else if (type === 'Organization') { return this.createOrganization(attrs); } }, - newModel(attrs, type) { + newOwner(attrs, type) { if (!type) { throw new Error('Type is required'); } - this.modelType = type; + this.ownerType = type; - if (type === 'user') { + if (type === 'User') { return this.newUser(attrs); - } else if (type === 'organization') { + } else if (type === 'Organization') { return this.newOrganization(attrs); } @@ -39,15 +39,15 @@ let SluggedRouteModel = Model.extend({ Object.defineProperty(SluggedRouteModel.prototype, 'model', { get () { - if (this.modelType === 'user') { + if (this.ownerType === 'user') { return this.user; - } else if (this.modelType === 'organization') { + } else if (this.ownerType === 'organization') { return this.organization; } }, set (model) { - this.modelType = model.modelName; + this.ownerType = model.modelName; if (model.modelName === 'user') { this.user = model; @@ -61,19 +61,19 @@ Object.defineProperty(SluggedRouteModel.prototype, 'model', { Object.defineProperty(SluggedRouteModel.prototype, 'modelId', { get () { - if (this.modelType) { - return this.attrs[this.modelType + 'Id']; + if (this.ownerType) { + return this.attrs[this.ownerType + 'Id']; } } }); -Object.defineProperty(SluggedRouteModel.prototype, 'modelType', { +Object.defineProperty(SluggedRouteModel.prototype, 'ownerType', { get () { - return this.attrs.modelType; + return this.attrs.ownerType; }, set (type) { - this.attrs.modelType = type; + this.attrs.ownerType = type; } }); diff --git a/package.json b/package.json index 114cce27e..1e6807fb4 100644 --- a/package.json +++ b/package.json @@ -42,7 +42,9 @@ "ember-cli-release": "0.2.8", "ember-cli-sass": "^5.2.1", "ember-cli-sri": "^1.2.0", + "ember-cli-typekit": "0.0.4", "ember-cli-uglify": "^1.2.0", + "ember-click-outside": "0.0.3", "ember-data": "1.13.15", "ember-disable-proxy-controllers": "^1.0.1", "ember-export-application-global": "^1.0.4", diff --git a/public/assets/images/icons/cc-logo-small.png b/public/assets/images/icons/cc-logo-small.png new file mode 100644 index 0000000000000000000000000000000000000000..7fd35e9bf1428f6c1ac1384dcd1916d5b68b53b9 GIT binary patch literal 1966 zcmV;f2T}NmP)_9j8d&{EJEEl@~I0P#$yB-$EOK&2{GYfA+Yp+co>kKK8GZ_3QhOlJ?& zm(0HZe&77>f5$A-zije*N~bQGtVx={1X|)D8jL4EOuwT)B8d(OL7!58`iQgWWTc@G zBlaeLp)_izMN*@w7%Wdp{Kn8=)C056&~KtVdXrh_ZJsd&lC6|P$EX+sm*P3c7U3)z z4A_L{2>{vvK$_tm6^PtN*CPXO0KgE#>Hp{f;#-zf#QF4tiWd=x+)GOU;(5%q$8B%~ zYlLpQkkkPE?`LWw?o7R;<8|{fz!cc1`svf4YiB_-cpU9;1tR4%kG5}1B@%a+(;E(* zl4-T@P$iG015Uk@*X!sT8m|RuF@Vp|d5e*n9z3POA~VVC^4%4Wes}ILQxGDmIG%Dm zd9*YZ5XudMNWPSId-7=}=sY~1gu9Y}ZkDP%*QXT-xCcO|k{ zy|LNm4w*jT!xMomEnTwS6rKfv8jzCt;f(tAZEkKl5KPH!4&|DL(x!ukY{{@$0|J4b zckxyU8`^p%wRYhkJBy}3V4~+;w0Fb$aBgDEb6Mvj8{@489u^f={-po!rQprovAV4r zVC1J(n{c!aLeM?A`!=o%XQSkzQw!dx`j8Pf9$5CPxns^7F)a;CaY;zuOI54P^h5@u zK(G+=bN6hjbox7aEOmK>Rfv_c-OBfL1aH9X_Or?iy84&`<33HjAb3H}o(*da`$)aO zyqhUkhEj@eZ4OK~*?0$2eXM)ZZ;s69Aea{B?Aci1Fb~TLIN5+epl(-Y1JE3k2}8Vt z%ot!42+YT#oV}Y~4`c2sMr}xSy)&Cv2pbUWy*9mR=pX>*s$MW$jMed)Lmibh&y=W^ z(G7UV{qtq~#Up|=abt9AhXpdiN3617pvRs8NjAOJ)=Ce2`rcaa$l(RJ482xT*D}wl zi-lIjY8yxs_L{-*-xw{>z!i&h5V{>I1iRPk%j#S1j^=083oO^eRsl;MW96|YzHj3l z%)loX)_O-}71+c8gU^hLhw7W>+T;-!EE2A>>XF(hRz58$^&?!fI;=8-PFoHTm-y@; z3?;x|J6l^)*F47%Me0RD0>6)yfoz^>Qs$0aH6q(!Uuv#8fn2|2@J64ud_ise=rAA* zBYm2<@}atevkg(WUhH4;>mPt&yL|<;psm=_lKMZ70OqnDuzcap_|e(!nQ)E-FnG_( zlFtv`HtGatk*tEM2-Jfi5AlIEI_fM{7=^F$Psx|oghs0L5#5Vjdr~W?Oga@V9+p?*d{gacFt@FxKZn}6^S zzn7SH;m)#A+~;#5d5gRSW2JT$df!&DY(SL2w{qD1lC&2U-8QJQ)}MS$0rn9;Du0fD zy7%l45f#YM5^I9$qW%mp;c{z)(-V2Ut3_0!Bw0n9{1nMt#)~A zT))k7bWjFYVfY(D9;{(S-RTNsQf;mGi=$Uo;ASy9;A*03fqw44vLHA3%WZf@K3qkNR8U zF#g7f!woo{v|u_u^Lzi6kkb9X73Nbyh?%FS@z#hy)L=;Gby)vZn9d7qApSGM4PFvO zr;2#L3T01|OYn|D{^VRCd@Q&G%hmPc$i*$J2#0J7GB>Mqx)@`CP{vU+VvRU(vrO}o zR*YGliisVkZl_j#SeiM2Rrfhwq-HpY^$mr6mvd}^Tt-OVimB=hPDaUJjsi@_n-0cm zNXM%M08kx}_-ld>0l=>Kex46c-pqfwgfT_`0QDoL`Erlhpa1{>07*qoM6N<$f_1jR AIsgCw literal 0 HcmV?d00001 diff --git a/public/assets/images/icons/cc-logo-small@2x.png b/public/assets/images/icons/cc-logo-small@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..0c7a64ef71de967a37965c2c851fb0a725772e81 GIT binary patch literal 4273 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 VAbwV`UrGP~002ovPDHLkV1kBt<0k+B literal 0 HcmV?d00001 diff --git a/public/assets/images/icons/cc-logo@2x.png b/public/assets/images/icons/cc-logo@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..2c8558c5c6a89a2c09f6a193b606c54dbf8a6b31 GIT binary patch literal 14336 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 BD7gRt literal 0 HcmV?d00001 diff --git a/public/assets/images/icons/link.png b/public/assets/images/icons/link.png new file mode 100644 index 0000000000000000000000000000000000000000..0dad753ae4acc9d0a02241d7d0c72ee1050e7407 GIT binary patch literal 573 zcmV-D0>b@?P)Nkl*|->(VHjp32DK5y4{j(Ia#5CrP(dguUDVs}h>D45FPwYc_c_n=o{#q` zp8r^`>*}Ra=?Ojt-H*LdRrOIQ6sjxB^tD=TrPXTfAig=xRTSlyVHl=54bI!Q+wH_i z+$8Hdb`st;nBQ@nmrE}g3GKE- z>>efFCw3akL3~K&Z^E^0`*^3*NfVdj?mriB^I^zalzm5{qjd)IA@;Y4uOiNJzQDOc zTTdq~h!}7myNWfrD9*qt{eC}&e+Oo9aTb3btAJgTc?^eWS=JE*2Lwd?g&{>WP5X-Y zx@nrv5H`sAR>bh$3s1y&k3wbxFasgV`7>{OB^V5TEEbDj@I2;yi zxKzNTi1Br@Mnx|gnOzR0v#cpfr+pbEEKE?*h~Gap{$1PQbmcyiPU z%Ae(-Z+nbZtJPE8ZZ}Rln^e>oF)slGDTGI<0Gy{7P2n7B}e9`&~*Jb93V;w6;00000 LNkvXXu0mjfUXuHW literal 0 HcmV?d00001 diff --git a/public/assets/images/icons/link@2x.png b/public/assets/images/icons/link@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..746a1c568de118f2c8f34c90a65f5701aa826f77 GIT binary patch literal 1194 zcmV;b1XcTqP)Tb8vJJ_5f4eN|CW(V;_Hy0TKa zx3@PCi^a|X_>x;K8o-q}RZvjyK`0dZUt+S>K!1OK$=uxBWoka@2Cmy^7_N;sAcbur>B!W$|i9uK7q2bvMCDwu8P7>n&9Fq zDJhxtcsz|-oQSXZ3`0h0US3|!?Ck8@089Yho~nr7@4qoIF(F@qI{6|cq{_*vU51r{ zLt4@C6<>i?>|wgsxn+%{;H<{0G@eJX7O_O5>+ag&zpDl&w7 zXyUZ)B!3(FStwVt$7pmbr*>({86dfgq(7jv;8J{ke*RhEYH{q_kxlS8jlL`35|^?8 zlAp$Z1uBB{6`}iDuh$!bzmDUM?VQ5)oeJdGcW~q;xa_>1Q#zyKT(5xSjU-=&0xHnv zU&G($5s50h%m={F`+UAz;c$3N?AUg}zK5KFLykf9-c_UVx7gnv@kHA zI0i$YX&+b7NF#@k^)F?F&=bL6@VGp`Mw+W?QRpX%)M(x|I^V77DMj+UJv}{Pcp-F` zrf<`f^G8U^M9twNv0Bq}*!`!vzLX4D9DmPJG3k6BimX#X$O*1vQ0Z>GudmO~u8ZA# znb>3q$5kS-#V^_hrJJ02X<%UQ@k2F&e9ZJ`qRR>Vr62-1y17Q^78TsJ6$henBOx%M)^#OiCxyv88vd`etH72G~`K@h6>>SLS4{VfKOwScRg$C0Sp+jENcMfOEF&t z-hgpLJUIbyJ}#he$1p*M@u6n3*(*3VxECVa=WW{_A?~_vA0}2r4)H-%RbOy!a3(}F zjN^sgnx=W`c^;i+TI67UE-=N!Kv9%KBY06X)PF3c`}WW zuN8~MXRasrfJ97_ahYxq4PHbT&33t5))7B>E_sBwC`7?JcF2o>1MJmwGu3n$kbo&B z!VzJ%4sb6l#uNC_$u}b>2!Kg;aNv0)_(MZtfd{@FgG`M1n4~@e%&My=&D213nL^xV^)#4f;`O5 zslL!_w&^v+e4MpNKw_NMj@Fs;d}!5~qm2MNP!J=aguo#u+rq-a zVQeBnV$QQe%SCJTmk5knL0X2u*=#nuUCVl=acw5aO)U%aD?zucvfTpC`6*Rs|2=Ith3Tsp0nPs<$s*VXVqlK4hPN5@+)2S);i<3$3^cm=WA@%fraB+gs)lI%)AH4lRINRDng zeEQnj+J3oVw9T;yH9*Z=%O_k*)#*G3$)8bp2RwlWz;y?75Mvj_R;n?^JR6NhPgr!W zqAvo*+wCRSNEjhHK=GO%M-D_l1&qhzeI%SW1z)7_`b~16WncpB?d?Cfk^`nJ z!xY|UlN@O2s}EQsLLkhioBHZ$Zfa`U;`GjM^^@elJD`FX#iqV^dRLNM#rg|XZT~2! SJ5q4~0000 zrBdm6)H1&BXWH%dA+Fhy{KttTf0L69ZhO6HW0UTr?j1})*b?l?gUSim6JKv2cK#LwWnD*xG} zl$#v8Dk)5YBgBys(J%`gg9f+;BMtqFD#%nSH39GU*BTIy%4q=`G?g!l#o{f~YP#@C zek>FU_wfMruqs{z#lF5f)n@@K>kni8b6_Pf`n1|#0Fd!1*f#w~Whla=h y1#aLR4P^${2xnjueuD4dQ_g(WKW1|7-}oPvikKg!=Tiaz0000QYbI!f@SSW9fB&XH zATX*Dz}SZK@!HzjtGZF4^TENvwKFp_H;J>$$j556zO&hEjc&I)5{tzecy7Y(HGC&k z0%Wrn=T5-(U@+Kf6vPZ0A0Kx`qtOul8_aA3hcqc#ibZ{Jxm-1Vzkf;|WNi7PEa&_B z`r2ma1u4ke@{vm*Ar>K#mpmSf$KwqYepGb;(%XQtcWPkNRaRDZth%~-B8?eo{5;^F z5r@&b3HB4*jf^-Pj=FRQKI+C~x+jrHge5~AH<)h)D~4P`xGAugGf`hvRTZ<(2JM~I zy7G|7b*emtgB-BCoR2TM0YO$LlWJ>qINyB4>-BE0sHl)mSkiBSooeNkS!d==62C}7 z4(c;x42d8CoFLwXsb<8?%>I^`<9JwcC=scW@TPFU59zk`0!}MGSOF3t&pX%|| zArydZctUnN^u-d)r#OIX^s}ZqAViR#Kuwi^G!qiy$2z$zWD?*VkI)|ZB#&e~mVR<0 zH)JLOSs;OLPN#D}(=$lcM@QBdGpM|W_V?t*NPh_xj*)YEdU}+HWx0k+@O;52Mpg&p z_lVIYBg4gD6xK=g-9|C85)cCnTXDE$WV{fJSQF*t<;`Zka}vNI#Z{>VheOC4Gsi-3 zVkPA!R9RX1)yzKg9WXNB)wB)|53lF)aEJBHMmFSQ9~v48a2cs*O}1d87!@-h9wW{XzAG;Z@v~CpHZB$CFb?9pLfT%4 zW3COp-@)4PzYKXicirq+k%hxydH>~3--6Vc8Jpm*z&q@A`?Y*`NF)C_)eZ~{_(-Cj zu1F#^$SSJyg2~-$lDYg&Cy@x)1F9|MryH8Tef$QdOg;B*81WDQ0000 { - assert.expect(3); - - // server.create uses factories. server.schema..create does not - let organization = server.schema.organization.create({ slug: 'test_organization' }); - let sluggedRoute = server.schema.sluggedRoute.create({ slug: 'test_organization', modelType: 'organization' }); - let projectId = server.create('project').id; - - // need to assign polymorphic properties explicitly - // TODO: see if it's possible to override models so we can do this in server.create - sluggedRoute.model = organization; - sluggedRoute.save(); - - let project = server.schema.project.find(projectId); - project.organization = organization; - project.save(); - - let post = project.createPost({ title: "Test title", body: "Test body", postType: "issue", number: 1 }); - - visit(`/${organization.slug}/${project.slug}/posts/${post.number}`); - - andThen(() => { - assert.equal(find('.post-details .title').text().trim(), post.title); - assert.equal(find('.post-details .body').text().trim(), post.body); - assert.equal(find('.post-details.issue .post-icon').length, 1, 'Post icon is rendered'); - }); -}); diff --git a/tests/acceptance/projects-test.js b/tests/acceptance/projects-test.js index 1a5811ca1..46f6195fd 100644 --- a/tests/acceptance/projects-test.js +++ b/tests/acceptance/projects-test.js @@ -17,7 +17,7 @@ test('It renders all the required ui elements', (assert) => { assert.expect(3); let sluggedRoute = server.schema.sluggedRoute.create({ slug: 'test_organization' }); - let organization = sluggedRoute.createModel({ slug: 'test_organization' }, 'organization'); + let organization = sluggedRoute.createOwner({ slug: 'test_organization' }, 'Organization'); sluggedRoute.save(); for (let i = 0; i < 5; i++) { organization.createProject({ diff --git a/tests/acceptance/signup-test.js b/tests/acceptance/signup-test.js index b51d2fe6d..c19773e4f 100644 --- a/tests/acceptance/signup-test.js +++ b/tests/acceptance/signup-test.js @@ -55,13 +55,7 @@ test('Succesful signup is possible', (assert) => { signUpDone(); - return { - data: { - id: 1, - type: "users", - attributes: params - } - }; + return db.user.create(params); }); let signInDone = assert.async(); @@ -97,13 +91,7 @@ test('Succesful signup also logs user in', (assert) => { signUpDone(); - return { - data: { - id: 1, - type: "users", - attributes: params - } - }; + return db.user.create(params); }); let signInDone = assert.async(); diff --git a/tests/acceptance/slugged-route-test.js b/tests/acceptance/slugged-route-test.js index 65ed794ce..2535e3a06 100644 --- a/tests/acceptance/slugged-route-test.js +++ b/tests/acceptance/slugged-route-test.js @@ -17,7 +17,7 @@ test("It renders user details when the sluggedRoute model is a user", function(a assert.expect(1); let sluggedRoute = server.schema.sluggedRoute.create({ slug: 'test_user' }); - sluggedRoute.createModel({ username: 'test_user' }, 'user'); + sluggedRoute.createOwner({ username: 'test_user' }, 'User'); sluggedRoute.save(); visit('/test_user'); @@ -30,7 +30,7 @@ test("It renders organization details when the sluggedRoute model is an organiza assert.expect(1); let sluggedRoute = server.schema.sluggedRoute.create({ slug: 'test_organization' }); - sluggedRoute.createModel({ slug: 'test_organization' }, 'organization'); + sluggedRoute.createOwner({ slug: 'test_organization' }, 'Organization'); sluggedRoute.save(); visit('/test_organization'); @@ -57,12 +57,12 @@ test("It renders a 404 error when no slugged route exists", function(assert) { andThen(function() { assert.equal(find('.error-wrapper').length, 1, 'error-wrapper component is rendered'); assert.equal(find('.error-wrapper h1').text(), '404 Error', 'The 404 title is rendered'); - assert.equal($('html').attr('class'), 'warning', 'The class of the html element is correct'); + assert.ok($('html').hasClass('warning'), 'The class of the html element is correct'); click('.error-wrapper a'); }); andThen(function() { assert.equal(find('.error-wrapper').length, 0, 'error-wrapper component is not rendered'); - assert.equal($('html').attr('class'), '', 'The class of the html element is unset'); + assert.notOk($('html').hasClass('warning'), 'The class of the html element is unset'); }); -}); \ No newline at end of file +}); diff --git a/tests/integration/components/error-wrapper-test.js b/tests/integration/components/error-wrapper-test.js index 6f382cc36..0675d87de 100644 --- a/tests/integration/components/error-wrapper-test.js +++ b/tests/integration/components/error-wrapper-test.js @@ -25,9 +25,9 @@ test('it renders all required elements for the 404 case', function(assert) { assert.equal(this.$('.not-found-img').length, 1, 'The 404 image renders'); assert.equal(this.$('h1').text().trim(), '404 Error', 'The title renders'); assert.equal(this.$('p:first').text().trim(), "We can't find the page you're looking for.", 'The body renders'); - assert.equal(this.$('a.button').text().trim(), "Go Home", 'The button renders'); - assert.equal($('html').attr('class'), "warning", 'The html element has the right class'); - assert.notEqual($('html').css('background-color'), "rgba(0, 0, 0, 0)", 'The html element does not have a white background'); + assert.equal(this.$('a.button').text().trim(), 'Go Home', 'The button renders'); + assert.ok($('html').hasClass('warning'), 'The html element has the right class'); + assert.notEqual($('html').css('background-color'), 'rgba(0, 0, 0, 0)', 'The html element does not have a white background'); }); test('it renders all required elements for the general error case', function(assert) { @@ -44,8 +44,8 @@ test('it renders all required elements for the general error case', function(ass assert.equal(this.$('.server-error-img').length, 1, 'The general error image renders'); assert.equal(this.$('h1').text().trim(), 'Server Error', 'The title renders'); - assert.equal(this.$('p:first').text().trim(), "Something went wrong. Try again and if the problem persists, please report your problem and mention what caused it.", 'The body renders'); - assert.equal(this.$('a.button').text().trim(), "Go Home", 'The button renders'); - assert.equal($('html').attr('class'), "danger", 'The html element has the right class'); - assert.notEqual($('html').css('background-color'), "rgba(0, 0, 0, 0)", 'The html element does not have a white background'); -}); \ No newline at end of file + assert.equal(this.$('p:first').text().trim(), 'Something went wrong. Try again and if the problem persists, please report your problem and mention what caused it.', 'The body renders'); + assert.equal(this.$('a.button').text().trim(), 'Go Home', 'The button renders'); + assert.ok($('html').hasClass('danger'), 'The html element has the right class'); + assert.notEqual($('html').css('background-color'), 'rgba(0, 0, 0, 0)', 'The html element does not have a white background'); +}); diff --git a/tests/integration/components/slugged-route-model-details-test.js b/tests/integration/components/slugged-route-model-details-test.js index fb4cbf57b..4045c376e 100644 --- a/tests/integration/components/slugged-route-model-details-test.js +++ b/tests/integration/components/slugged-route-model-details-test.js @@ -13,7 +13,7 @@ test('it renders', function(assert) { test('when the slugged route is an organization, it renders the organization component', function(assert) { assert.expect(1); - let sluggedRoute = { modelType: 'organization' }; + let sluggedRoute = { ownerType: 'Organization' }; this.set('sluggedRoute', sluggedRoute); this.render(hbs`{{slugged-route-model-details sluggedRoute=sluggedRoute}}`); @@ -25,7 +25,7 @@ test('when the slugged route is an organization, it renders the organization com test('when the slugged route is a user, it renders the user component', function(assert) { assert.expect(1); - let sluggedRoute = { modelType: 'user' }; + let sluggedRoute = { ownerType: 'User' }; this.set('sluggedRoute', sluggedRoute); this.render(hbs`{{slugged-route-model-details sluggedRoute=sluggedRoute}}`); diff --git a/tests/integration/components/user-menu-test.js b/tests/integration/components/user-menu-test.js new file mode 100644 index 000000000..3b14459f0 --- /dev/null +++ b/tests/integration/components/user-menu-test.js @@ -0,0 +1,46 @@ +import { moduleForComponent, test } from 'ember-qunit'; +import hbs from 'htmlbars-inline-precompile'; +import Ember from 'ember'; + +const stubUser = Ember.Object.extend({ + id: 1, + username: 'tester', + photoThumbUrl: '/assets/images/twitter.png', + + atUsername: Ember.computed('username', function() { + return `@${this.get('username')}`; + }), + + twitterUrl: Ember.computed('twitter', function() { + return `https://twitter.com/${this.get('twitter')}`; + }) +}).create(); + +moduleForComponent('user-menu', 'Integration | Component | user menu', { + integration: true, +}); + +test('it renders properly', function(assert) { + assert.expect(10); + + this.set('user', stubUser); + this.render(hbs`{{user-menu model=user}}
Outside
`); + + assert.equal(this.$('.user-menu').length, 1, "The component's element renders."); + + assert.equal(this.$('img.avatar').length, 1, "The user's avatar renders"); + assert.equal(this.$('img.avatar').attr('src'), stubUser.get('photoThumbUrl'), 'The avatar has the correct source'); + assert.equal(this.$('img.avatar').attr('alt'), stubUser.get('atUsername'), 'The avatar has the correct alt'); + + assert.equal(this.$('.user-menu.menu-hidden').length, 1, 'The menu is initially hidden'); + + this.$('.user-menu-select').click(); + + assert.equal(this.$('.user-menu.menu-visible').length, 1, 'The menu is now visible'); + + assert.equal(this.$('a.slugged-route').length, 1, 'The link to the user route is rendered'); + assert.equal(this.$('a.profile').length, 1, 'The link to the user profile is rendered'); + assert.equal(this.$('a.logout').length, 1, 'The logout link is rendered'); + + assert.equal(this.$('.dropdown-footer').text().trim(), `Signed in as ${stubUser.get('username')}`, 'The username is rendered in the footer'); +}); diff --git a/tests/unit/models/model-test.js b/tests/unit/models/owner-test.js similarity index 83% rename from tests/unit/models/model-test.js rename to tests/unit/models/owner-test.js index 7e7fb856b..805cdd6a5 100644 --- a/tests/unit/models/model-test.js +++ b/tests/unit/models/owner-test.js @@ -1,6 +1,6 @@ import { moduleForModel, test } from 'ember-qunit'; -moduleForModel('model', 'Unit | Model | model', { +moduleForModel('owner', 'Unit | Model | owner', { // Specify the other units that are required for this test. needs: [] }); diff --git a/tests/unit/models/slugged-route-test.js b/tests/unit/models/slugged-route-test.js index 34eb02972..d1ba73fbd 100644 --- a/tests/unit/models/slugged-route-test.js +++ b/tests/unit/models/slugged-route-test.js @@ -2,7 +2,7 @@ import { moduleForModel, test } from 'ember-qunit'; moduleForModel('slugged-route', 'Unit | Model | slugged-route', { // Specify the other units that are required for this test. - needs: ['model:model'] + needs: ['model:owner'] }); test('it exists', function(assert) { diff --git a/tests/unit/models/user-test.js b/tests/unit/models/user-test.js index ba211109a..cb2e97354 100644 --- a/tests/unit/models/user-test.js +++ b/tests/unit/models/user-test.js @@ -2,7 +2,7 @@ import { moduleForModel, test } from 'ember-qunit'; moduleForModel('user', 'Unit | Model | user', { // Specify the other units that are required for this test. - needs: [] + needs: ['model:organization'] }); test('it exists', function(assert) {