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 @@
{{#each crate.owners as |owner|}}
-
- {{#user-link user=owner}}
- {{user-avatar user=owner size='medium-small'}}
- {{/user-link}}
+ {{#link-to 'user' owner.login}}
+ {{user-avatar user=owner size='medium-small'}}
+ {{/link-to}}
{{/each}}
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}}
+

+ {{/user-link}}
+
+
+
+
+ {{! TODO: reduce duplication with templates/crates.hbs }}
+
+
+
+
+ Displaying
+ {{ currentPageStart }}-{{ currentPageEnd }}
+ of {{ totalItems }} total results
+
+
+
+
+
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());