First pass at members list

no issue
- don't nest details route as it's not nested UI
- implement styled list of members
- add `<MemberAvatar>` component that generates random background colour and initials based on member name
- fixed generation of fake member details in mirage
This commit is contained in:
Kevin Ansfield 2019-01-24 19:34:32 +00:00
parent 76a1a98b48
commit 7ec48b36e0
17 changed files with 165 additions and 28 deletions

View File

@ -0,0 +1,32 @@
import Component from '@ember/component';
import {computed} from '@ember/object';
import {htmlSafe} from '@ember/string';
const stringToHslColor = function (str, saturation, lightness) {
var hash = 0;
for (var i = 0; i < str.length; i++) {
hash = str.charCodeAt(i) + ((hash << 5) - hash);
}
var h = hash % 360;
return 'hsl(' + h + ', ' + saturation + '%, ' + lightness + '%)';
};
export default Component.extend({
member: null,
backgroundStyle: computed('member.name', function () {
let name = this.member.name;
if (name) {
let color = stringToHslColor(name, 30, 80);
return htmlSafe(`background-color: ${color}`);
}
}),
initials: computed('member.name', function () {
let names = this.member.name.split(' ');
let intials = [names[0][0], names[names.length - 1][0]];
return intials.join('').toUpperCase();
})
});

View File

@ -0,0 +1,6 @@
import Controller from '@ember/controller';
import {alias} from '@ember/object/computed';
export default Controller.extend({
member: alias('model')
});

View File

@ -0,0 +1,31 @@
import Controller from '@ember/controller';
import {computed} from '@ember/object';
import {task} from 'ember-concurrency';
/* eslint-disable ghost/ember/alias-model-in-controller */
export default Controller.extend({
queryParams: ['page'],
meta: null,
members: null,
page: computed('meta.pagination.page', function () {
let page = this.get('meta.pagination.page');
if (!page || page === 1) {
return null;
}
return page;
}),
fetchMembers: task(function* () {
let results = yield this.store.query('member', {
page: this.page || 1,
limit: 15
});
this.set('meta', results.meta);
this.set('members', results);
})
});

View File

@ -64,9 +64,8 @@ Router.map(function () {
this.route('settings.integrations.unsplash', {path: '/settings/integrations/unsplash'});
this.route('settings.integrations.zapier', {path: '/settings/integrations/zapier'});
this.route('members', function () {
this.route('details', {path: ':member_id'});
});
this.route('members');
this.route('member', {path: '/members/:member_id'});
this.route('subscribers', function () {
this.route('new');

View File

@ -0,0 +1,9 @@
import AuthenticatedRoute from 'ghost-admin/routes/authenticated';
export default AuthenticatedRoute.extend({
// TODO: add model method to load member if not passed in
titleToken() {
return this.controller.get('member.name');
}
});

View File

@ -22,5 +22,10 @@ export default AuthenticatedRoute.extend({
return this.transitionTo('posts');
}
});
},
setupController(controller) {
this._super(...arguments);
controller.fetchMembers.perform();
}
});

View File

@ -1,3 +0,0 @@
import AuthenticatedRoute from 'ghost-admin/routes/authenticated';
export default AuthenticatedRoute.extend({});

View File

@ -1,7 +0,0 @@
import AuthenticatedRoute from 'ghost-admin/routes/authenticated';
export default AuthenticatedRoute.extend({
model() {
return this.store.findAll('member');
}
});

View File

@ -51,7 +51,7 @@
</li>
<li>{{#link-to "team" data-test-nav="team"}}{{svg-jar "account-group"}}Team{{/link-to}}</li>
{{#if (and config.enableDeveloperExperiments (gh-user-can-admin session.user))}}
<li>{{#link-to "members" data-test-nav="members"}}{{svg-jar "email"}}Members{{/link-to}}</li>
<li>{{#link-to "members" current-when="members member" data-test-nav="members"}}{{svg-jar "email"}}Members{{/link-to}}</li>
{{/if}}
{{#if (and feature.subscribers (gh-user-can-admin session.user))}}
<li>{{#link-to "subscribers" data-test-nav="subscribers"}}{{svg-jar "email"}}Subscribers{{/link-to}}</li>

View File

@ -0,0 +1,3 @@
<div class="flex items-center justify-center br-100" style={{this.backgroundStyle}} ...attributes>
<span class="block white f5 fw7">{{this.initials}}</span>
</div>

View File

@ -0,0 +1,13 @@
<section class="gh-canvas">
<header class="gh-canvas-header">
<h2 class="gh-canvas-title" data-test-screen-title>
{{link-to "Members" "members" data-test-link="members"}}
<span>{{svg-jar "arrow-right"}}</span>
{{member.name}}
</h2>
</header>
<section class="view-container">
</section>
</section>

View File

@ -1 +1,36 @@
{{outlet}}
<section class="gh-canvas">
<header class="gh-canvas-header">
<h2 class="gh-canvas-title" data-test-screen-title>Members</h2>
<div class="view-actions">
<span>{{this.meta.pagination.total}} Members</span>
</div>
</header>
<section class="view-container">
{{#if this.members}}
{{!-- members list, styles taken from .apps-grid --}}
<div class="flex flex-row flex-wrap items-start ba br3">
{{#each this.members as |member index|}}
<div class="flex-grow-1 flex-shrink-1" style="flex-basis: 100%">
{{#link-to "member" member}}
<article class="flex items-center justify-between pa4 {{if index "bt"}}">
<div class="flex items-center">
<MemberAvatar @member={{member}} class="w10 h10 mr4" />
<div class="flex flex-column">
<h3 class="ma0 f5 fw7">{{member.name}}</h3>
<p class="ma0 pa0 f7">{{member.email}}</p>
</div>
</div>
<div class="flex items-center">{{svg-jar "arrow-right" class="w4"}}</div>
</article>
{{/link-to}}
</div>
{{/each}}
</div>
{{else}}
{{#unless this.fetchMembers.isRunning}}
<p>No members found.</p>
{{/unless}}
{{/if}}
</section>
</section>

View File

@ -1,10 +0,0 @@
<section class="gh-canvas">
<header class="gh-canvas-header">
<h2 class="gh-canvas-title" data-test-screen-title>Members</h2>
<div class="view-actions"></div>
</header>
<section class="view-container">
<p>...</p>
</section>
</section>

View File

@ -24,15 +24,13 @@ export default function () {
// this.urlPrefix = ''; // make this `http://localhost:8080`, for example, if your API is on a different server
this.namespace = '/ghost/api/v2/admin'; // make this `api`, for example, if your API is namespaced
this.timing = 400; // delay for each request, automatically set to 0 during testing
this.timing = 1000; // delay for each request, automatically set to 0 during testing
this.logging = true;
// Mock endpoints here to override real API requests during development, eg...
// this.put('/posts/:id/', versionMismatchResponse);
// mockTags(this);
// this.loadFixtures('settings');
this.createList('member', 200);
mockMembers(this);
// keep this line, it allows all other API requests to hit the real server

View File

@ -8,4 +8,6 @@ export default function (server) {
server.createList('tag', 100);
server.create('integration', {name: 'Demo'});
server.createList('member', 125);
}

View File

@ -0,0 +1,24 @@
import { expect } from 'chai';
import { describe, it } from 'mocha';
import { setupComponentTest } from 'ember-mocha';
import hbs from 'htmlbars-inline-precompile';
describe('Integration | Component | member-avatar', function() {
setupComponentTest('member-avatar', {
integration: true
});
it('renders', function() {
// Set any properties with this.set('myProperty', 'value');
// Handle any actions with this.on('myAction', function(val) { ... });
// Template block usage:
// this.render(hbs`
// {{#member-avatar}}
// template content
// {{/member-avatar}}
// `);
this.render(hbs`{{member-avatar}}`);
expect(this.$()).to.have.length(1);
});
});