diff --git a/app/controllers/user.js b/app/controllers/user.js new file mode 100644 index 00000000000..bfc99c8d576 --- /dev/null +++ b/app/controllers/user.js @@ -0,0 +1,19 @@ +import Ember from 'ember'; +import PaginationMixin from '../mixins/pagination'; + +const { computed } = Ember; + +// TODO: reduce duplication with controllers/crates + +export default Ember.Controller.extend(PaginationMixin, { + queryParams: ['page', 'per_page', 'sort'], + page: '1', + per_page: 10, + sort: 'alpha', + + totalItems: computed.readOnly('model.crates.meta.total'), + + currentSortBy: computed('sort', function() { + return (this.get('sort') === 'downloads') ? 'Downloads' : 'Alphabetical'; + }), +}); diff --git a/app/router.js b/app/router.js index d858f0d5c9a..8e48c6c8ca7 100644 --- a/app/router.js +++ b/app/router.js @@ -27,6 +27,7 @@ Router.map(function() { this.route('crates'); this.route('following'); }); + this.route('user', { path: '/users/:user_id' }); this.route('install'); this.route('search'); this.route('dashboard'); diff --git a/app/routes/user.js b/app/routes/user.js new file mode 100644 index 00000000000..a8a0f79d850 --- /dev/null +++ b/app/routes/user.js @@ -0,0 +1,37 @@ +import Ember from 'ember'; + +export default Ember.Route.extend({ + queryParams: { + page: { refreshModel: true }, + sort: { refreshModel: true }, + }, + data: {}, + + setupController(controller, model) { + this._super(controller, model); + + controller.set('fetchingFeed', true); + controller.set('crates', this.get('data.crates')); + }, + + model(params) { + const { user_id } = params; + return this.store.find('user', user_id).then( + (user) => { + params.user_id = user.get('id'); + return Ember.RSVP.hash({ + crates: this.store.query('crate', params), + user + }); + }, + (e) => { + if (e.errors.any(e => e.detail === 'Not Found')) { + this + .controllerFor('application') + .set('nextFlashError', `User '${params.user_id}' does not exist`); + return this.replaceWith('index'); + } + } + ); + }, +}); diff --git a/app/styles/crate.scss b/app/styles/crate.scss index 81aa6b3de9d..9e9d22d1192 100644 --- a/app/styles/crate.scss +++ b/app/styles/crate.scss @@ -11,7 +11,10 @@ @include display-flex; @include align-items(center); } - h1 { padding-left: 10px; } + h1 { + padding-left: 10px; + padding-right: 10px; + } h2 { color: $main-color-light; padding-left: 10px; } .right { @include flex(2); diff --git a/app/templates/crate/version.hbs b/app/templates/crate/version.hbs index a39c698cd13..42356ec38ec 100644 --- a/app/templates/crate/version.hbs +++ b/app/templates/crate/version.hbs @@ -109,9 +109,9 @@ diff --git a/app/templates/user.hbs b/app/templates/user.hbs new file mode 100644 index 00000000000..70fc80623f2 --- /dev/null +++ b/app/templates/user.hbs @@ -0,0 +1,67 @@ +
+ {{user-avatar user=model.user size='medium'}} +

+ {{ model.user.login }} +

+ {{#user-link user=model.user}} + GitHub profile + {{/user-link}} +
+ +
+
+ {{! TODO: reduce duplication with templates/crates.hbs }} + +
+ + +
+ Sort by + {{#rl-dropdown-container class="dropdown-container"}} + {{#rl-dropdown-toggle tagName="a" class="dropdown"}} + + {{ currentSortBy }} + + {{/rl-dropdown-toggle}} + + {{#rl-dropdown tagName="ul" class="dropdown" closeOnChildClick="a:link"}} +
  • + {{#link-to (query-params sort="alpha")}} + Alphabetical + {{/link-to}} +
  • +
  • + {{#link-to (query-params sort="downloads")}} + Downloads + {{/link-to}} +
  • + {{/rl-dropdown}} + {{/rl-dropdown-container}} +
    +
    + +
    + {{#each model.crates as |crate|}} + {{crate-row crate=crate}} + {{/each}} +
    + + +
    +
    diff --git a/public/assets/GitHub-Mark-32px.png b/public/assets/GitHub-Mark-32px.png new file mode 100644 index 00000000000..8b25551a979 Binary files /dev/null and b/public/assets/GitHub-Mark-32px.png differ diff --git a/src/lib.rs b/src/lib.rs index 3d01759d200..33596374d86 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -100,6 +100,7 @@ pub fn middleware(app: Arc) -> MiddlewareBuilder { api_router.get("/versions/:version_id", C(version::show)); api_router.get("/keywords", C(keyword::index)); api_router.get("/keywords/:keyword_id", C(keyword::show)); + api_router.get("/users/:user_id", C(user::show)); let api_router = Arc::new(R404(api_router)); let mut router = RouteBuilder::new(); diff --git a/src/user/mod.rs b/src/user/mod.rs index 3d7a2a68f7f..904e1a09049 100644 --- a/src/user/mod.rs +++ b/src/user/mod.rs @@ -2,6 +2,7 @@ use std::collections::HashMap; use conduit::{Request, Response}; use conduit_cookie::{RequestSession}; +use conduit_router::RequestParams; use pg::GenericConnection; use pg::rows::Row; use pg::types::Slice; @@ -288,6 +289,20 @@ pub fn me(req: &mut Request) -> CargoResult { Ok(req.json(&R{ user: user.clone().encodable(), api_token: token })) } +/// Handles the `GET /users/:user_id` route. +pub fn show(req: &mut Request) -> CargoResult { + let name = &req.params()["user_id"]; + let conn = try!(req.tx()); + let user = try!(User::find_by_login(conn, &name)); + + #[derive(RustcEncodable)] + struct R { + user: EncodableUser, + } + Ok(req.json(&R{ user: user.clone().encodable() })) +} + + /// Handles the `GET /me/updates` route. pub fn updates(req: &mut Request) -> CargoResult { let user = try!(req.user());