From 3a6bffa05db4cbc7dd6106473cecf82e87f14085 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?R=C3=A9gis=20Hanol?= <regis@hanol.fr>
Date: Tue, 14 Jan 2014 01:02:14 +0100
Subject: [PATCH] FEATURE: better top pages

---
 app/assets/javascripts/discourse.js           |  5 +-
 .../components/basic_topic_list_component.js  | 10 ++-
 .../controllers/list_categories_controller.js |  2 +-
 .../controllers/list_controller.js.erb        |  8 +-
 .../controllers/list_top_controller.js        | 20 +++--
 .../controllers/static_controller.js          |  6 +-
 app/assets/javascripts/discourse/lib/url.js   |  2 +-
 .../javascripts/discourse/models/category.js  |  4 +-
 .../discourse/models/category_list.js         | 29 ++-----
 .../discourse/models/nav_item.js.erb          | 19 ++---
 .../javascripts/discourse/models/top_list.js  | 34 --------
 .../discourse/models/top_list.js.erb          | 37 +++++++++
 .../discourse/models/topic_list.js            |  3 +-
 .../javascripts/discourse/models/user.js      |  2 -
 .../discourse/routes/application_routes.js    | 82 +++++++++++--------
 .../discourse/routes/discourse_route.js       |  2 +-
 .../discourse/routes/filtered_list_route.js   | 34 ++++----
 .../discourse/routes/list_categories_route.js | 52 ++++++------
 .../discourse/routes/list_category_route.js   | 37 +++++----
 .../discourse/routes/list_top_route.js        | 44 ++++++----
 .../discourse/routes/static_route.js          |  8 +-
 .../routes/user_topic_list_routes.js          |  5 +-
 .../components/basic-topic-list.js.handlebars |  2 +
 .../templates/list/categories.js.handlebars   | 29 +++----
 .../templates/list/top.js.handlebars          | 48 +++++++++++
 .../discourse/templates/top.js.handlebars     | 18 ----
 .../discourse/views/list/list_top_view.js     | 18 ++++
 .../discourse/views/nav_item_view.js          |  2 +-
 .../stylesheets/desktop/topic-list.scss       |  2 +
 app/controllers/list_controller.rb            | 55 ++++++++++---
 app/serializers/top_list_serializer.rb        | 15 +---
 config/locales/client.en.yml                  |  3 +
 config/routes.rb                              | 44 +++++-----
 .../20131223171005_create_top_topics.rb       | 11 ++-
 lib/topic_query.rb                            |  8 +-
 test/javascripts/lib/url_test.js              |  2 +-
 36 files changed, 400 insertions(+), 302 deletions(-)
 delete mode 100644 app/assets/javascripts/discourse/models/top_list.js
 create mode 100644 app/assets/javascripts/discourse/models/top_list.js.erb
 create mode 100644 app/assets/javascripts/discourse/templates/list/top.js.handlebars
 delete mode 100644 app/assets/javascripts/discourse/templates/top.js.handlebars
 create mode 100644 app/assets/javascripts/discourse/views/list/list_top_view.js

diff --git a/app/assets/javascripts/discourse.js b/app/assets/javascripts/discourse.js
index 59abdb633..97f4f1a31 100644
--- a/app/assets/javascripts/discourse.js
+++ b/app/assets/javascripts/discourse.js
@@ -88,9 +88,8 @@ window.Discourse = Ember.Application.createWithMixins(Discourse.Ajax, {
   },
 
   loginRequired: function() {
-    return (
-      Discourse.SiteSettings.login_required && !Discourse.User.current()
-    );
+    return Discourse.SiteSettings.login_required &&
+           !Discourse.User.current();
   }.property(),
 
   redirectIfLoginRequired: function(route) {
diff --git a/app/assets/javascripts/discourse/components/basic_topic_list_component.js b/app/assets/javascripts/discourse/components/basic_topic_list_component.js
index 41aada403..065383a06 100644
--- a/app/assets/javascripts/discourse/components/basic_topic_list_component.js
+++ b/app/assets/javascripts/discourse/components/basic_topic_list_component.js
@@ -22,10 +22,12 @@ Discourse.BasicTopicListComponent = Ember.Component.extend({
   }.observes('topicList'),
 
   _initFromTopicList: function(topicList) {
-    this.setProperties({
-      topics: topicList.get('topics'),
-      sortOrder: topicList.get('sortOrder')
-    });
+    if (topicList !== null) {
+      this.setProperties({
+        topics: topicList.get('topics'),
+        sortOrder: topicList.get('sortOrder')
+      });
+    }
   },
 
   init: function() {
diff --git a/app/assets/javascripts/discourse/controllers/list_categories_controller.js b/app/assets/javascripts/discourse/controllers/list_categories_controller.js
index 2f285bc94..4e10b6f5a 100644
--- a/app/assets/javascripts/discourse/controllers/list_categories_controller.js
+++ b/app/assets/javascripts/discourse/controllers/list_categories_controller.js
@@ -30,7 +30,7 @@ Discourse.ListCategoriesController = Discourse.ObjectController.extend({
 
   latestTopicOnly: function() {
     return this.get('categories').find(function(c) { return c.get('featuredTopics.length') > 1; }) === undefined;
-  }.property('categories.featuredTopics.length')
+  }.property('categories.@each.featuredTopics.length')
 
 });
 
diff --git a/app/assets/javascripts/discourse/controllers/list_controller.js.erb b/app/assets/javascripts/discourse/controllers/list_controller.js.erb
index e47473f21..9361cb74a 100644
--- a/app/assets/javascripts/discourse/controllers/list_controller.js.erb
+++ b/app/assets/javascripts/discourse/controllers/list_controller.js.erb
@@ -95,12 +95,12 @@ Discourse.ListController = Discourse.Controller.extend({
   // Put in the appropriate page title based on our view
   updateTitle: function() {
     if (this.get('filterMode') === 'categories') {
-      return Discourse.set('title', I18n.t('categories_list'));
+      Discourse.set('title', I18n.t('categories_list'));
     } else {
       if (this.present('category')) {
-        return Discourse.set('title', this.get('category.name').capitalize() + " " + I18n.t('topic.list'));
+        Discourse.set('title', this.get('category.name').capitalize() + " " + I18n.t('topic.list'));
       } else {
-        return Discourse.set('title', I18n.t('topic.list'));
+        Discourse.set('title', I18n.t('topic.list'));
       }
     }
   }.observes('filterMode', 'category'),
@@ -134,5 +134,5 @@ Discourse.ListController = Discourse.Controller.extend({
 });
 
 Discourse.ListController.reopenClass({
-  filters: <%= Discourse.filters.map(&:to_s) %>
+  FILTERS: <%= Discourse.filters.map(&:to_s) %>
 });
diff --git a/app/assets/javascripts/discourse/controllers/list_top_controller.js b/app/assets/javascripts/discourse/controllers/list_top_controller.js
index cc0b6d0a7..f71e0bfda 100644
--- a/app/assets/javascripts/discourse/controllers/list_top_controller.js
+++ b/app/assets/javascripts/discourse/controllers/list_top_controller.js
@@ -23,12 +23,18 @@ Discourse.ListTopController = Discourse.ObjectController.extend({
     return null;
   }.property(),
 
-  showThisYear: function() {
-    if (Discourse.User.current()) {
-      return !Discourse.User.currentProp("hasBeenSeenInTheLastMonth");
-    } else {
-      return true;
-    }
-  }.property()
+  hasDisplayedAllTopLists: Em.computed.and('content.yearly', 'content.monthly', 'content.weekly', 'content.daily'),
+
+  showMoreUrl: function(period) {
+    var url = "", category = this.get("category");
+    if (category) { url += category.get("url") + "/l"; }
+    url += "/top/" + period;
+    return url;
+  },
+
+  showMoreDailyUrl: function() { return this.showMoreUrl("daily"); }.property("category.url"),
+  showMoreWeeklyUrl: function() { return this.showMoreUrl("weekly"); }.property("category.url"),
+  showMoreMonthlyUrl: function() { return this.showMoreUrl("monthly"); }.property("category.url"),
+  showMoreYearlyUrl: function() { return this.showMoreUrl("yearly"); }.property("category.url"),
 
 });
diff --git a/app/assets/javascripts/discourse/controllers/static_controller.js b/app/assets/javascripts/discourse/controllers/static_controller.js
index f3f945b83..05b6d5c8c 100644
--- a/app/assets/javascripts/discourse/controllers/static_controller.js
+++ b/app/assets/javascripts/discourse/controllers/static_controller.js
@@ -37,12 +37,10 @@ Discourse.StaticController = Discourse.Controller.extend({
 });
 
 Discourse.StaticController.reopenClass({
-  pages: ['faq', 'tos', 'privacy', 'login'],
-  configs: {
+  PAGES: ['faq', 'tos', 'privacy', 'login'],
+  CONFIGS: {
     'faq': 'faq_url',
     'tos': 'tos_url',
     'privacy': 'privacy_policy_url'
   }
 });
-
-
diff --git a/app/assets/javascripts/discourse/lib/url.js b/app/assets/javascripts/discourse/lib/url.js
index 0303d9472..ca78faba9 100644
--- a/app/assets/javascripts/discourse/lib/url.js
+++ b/app/assets/javascripts/discourse/lib/url.js
@@ -165,7 +165,7 @@ Discourse.URL = Em.Object.createWithMixins({
     @param {String} path the path we're navigating to
   **/
   navigatedToHome: function(oldPath, path) {
-    var defaultFilter = "/" + Discourse.ListController.filters[0];
+    var defaultFilter = "/" + Discourse.ListController.FILTERS[0];
 
     if (path === "/" && (oldPath === "/" || oldPath === defaultFilter)) {
       // Refresh our list
diff --git a/app/assets/javascripts/discourse/models/category.js b/app/assets/javascripts/discourse/models/category.js
index 2dae55ed8..6710c995a 100644
--- a/app/assets/javascripts/discourse/models/category.js
+++ b/app/assets/javascripts/discourse/models/category.js
@@ -44,7 +44,7 @@ Discourse.Category = Discourse.Model.extend({
   }.property('url'),
 
   style: function() {
-    return "background-color: #" + this.get('category.color') + "; color: #" + (this.get('category.text_color')) + ";";
+    return "background-color: #" + this.get('category.color') + "; color: #" + this.get('category.text_color') + ";";
   }.property('color', 'text_color'),
 
   moreTopics: function() {
@@ -54,7 +54,7 @@ Discourse.Category = Discourse.Model.extend({
   save: function() {
     var url = "/categories";
     if (this.get('id')) {
-      url = "/categories/" + (this.get('id'));
+      url = "/categories/" + this.get('id');
     }
 
     return Discourse.ajax(url, {
diff --git a/app/assets/javascripts/discourse/models/category_list.js b/app/assets/javascripts/discourse/models/category_list.js
index 442870585..69ac54451 100644
--- a/app/assets/javascripts/discourse/models/category_list.js
+++ b/app/assets/javascripts/discourse/models/category_list.js
@@ -16,7 +16,7 @@ Discourse.CategoryList = Ember.ArrayProxy.extend({
   moveCategory: function(categoryId, position){
     Discourse.ajax("/category/" + categoryId + "/move", {
       type: 'POST',
-      data: {position: position}
+      data: { position: position }
     });
   }
 });
@@ -55,31 +55,20 @@ Discourse.CategoryList.reopenClass({
     return categories;
   },
 
-  list: function(filter) {
-    var self = this,
-        finder = null;
+  list: function() {
+    var self = this;
 
-    if (filter === 'categories') {
-      finder = PreloadStore.getAndRemove("categories_list", function() {
-        return Discourse.ajax("/categories.json");
-      });
-    } else {
-      finder = Discourse.ajax("/" + filter + ".json");
-    }
-
-    return finder.then(function(result) {
-      var categoryList = Discourse.TopicList.create();
-      categoryList.setProperties({
+    return PreloadStore.getAndRemove("categories_list", function() {
+      return Discourse.ajax("/categories.json");
+    }).then(function(result) {
+      return Discourse.CategoryList.create({
+        categories: self.categoriesFrom(result),
         can_create_category: result.category_list.can_create_category,
         can_create_topic: result.category_list.can_create_topic,
-        categories: self.categoriesFrom(result),
         draft_key: result.category_list.draft_key,
-        draft_sequence: result.category_list.draft_sequence
+        draft_sequence: result.category_list.draft_sequence,
       });
-      return categoryList;
     });
   }
 
 });
-
-
diff --git a/app/assets/javascripts/discourse/models/nav_item.js.erb b/app/assets/javascripts/discourse/models/nav_item.js.erb
index 6e61c8d77..09721d6e7 100644
--- a/app/assets/javascripts/discourse/models/nav_item.js.erb
+++ b/app/assets/javascripts/discourse/models/nav_item.js.erb
@@ -6,8 +6,6 @@
   @namespace Discourse
   @module Discourse
 **/
-var validNavNames = <%= Discourse.top_menu_items.map(&:to_s) %>;
-var validAnon = <%= Discourse.anonymous_top_menu_items.map(&:to_s) %>;
 
 Discourse.NavItem = Discourse.Model.extend({
 
@@ -69,26 +67,25 @@ Discourse.NavItem = Discourse.Model.extend({
 });
 
 Discourse.NavItem.reopenClass({
+  NAMES: <%= Discourse.top_menu_items.map(&:to_s) %>,
+  ANONYMOUS_NAMES: <%= Discourse.anonymous_top_menu_items.map(&:to_s) %>,
 
   // create a nav item from the text, will return null if there is not valid nav item for this particular text
   fromText: function(text, opts) {
-    var countSummary = opts.countSummary,
-        split = text.split(","),
+    var split = text.split(","),
         name = split[0],
         testName = name.split("/")[0];
 
-    if (!opts.loggedOn && !validAnon.contains(testName)) return null;
+    if (!opts.loggedOn && !Discourse.NavItem.ANONYMOUS_NAMES.contains(testName)) return null;
     if (!Discourse.Category.list() && testName === "categories") return null;
-    if (!validNavNames.contains(testName)) return null;
+    if (!Discourse.NavItem.NAMES.contains(testName)) return null;
 
-    opts = {
+    return Discourse.NavItem.create({
       name: name,
       hasIcon: name === "unread" || name === "starred",
       filters: split.splice(1),
-      category: opts.category
-    };
-
-    return Discourse.NavItem.create(opts);
+      category: opts.category,
+    });
   }
 
 });
diff --git a/app/assets/javascripts/discourse/models/top_list.js b/app/assets/javascripts/discourse/models/top_list.js
deleted file mode 100644
index bb67154b6..000000000
--- a/app/assets/javascripts/discourse/models/top_list.js
+++ /dev/null
@@ -1,34 +0,0 @@
-/**
-  A data model representing a list of top topic lists
-
-  @class TopList
-  @extends Discourse.Model
-  @namespace Discourse
-  @module Discourse
-**/
-
-Discourse.TopList = Discourse.Model.extend({});
-
-Discourse.TopList.reopenClass({
-  find: function() {
-    return PreloadStore.getAndRemove("top_list", function() {
-      return Discourse.ajax("/top.json");
-    }).then(function (result) {
-      var topList = Discourse.TopList.create({
-        can_create_topic: result.can_create_topic,
-        yearly: Discourse.TopicList.from(result.yearly),
-        monthly: Discourse.TopicList.from(result.monthly),
-        weekly: Discourse.TopicList.from(result.weekly),
-        daily: Discourse.TopicList.from(result.daily)
-      });
-      // disable sorting
-      topList.setProperties({
-        "yearly.sortOrder": undefined,
-        "monthly.sortOrder": undefined,
-        "weekly.sortOrder": undefined,
-        "daily.sortOrder": undefined
-      });
-      return topList;
-    });
-  }
-});
diff --git a/app/assets/javascripts/discourse/models/top_list.js.erb b/app/assets/javascripts/discourse/models/top_list.js.erb
new file mode 100644
index 000000000..d595ea67e
--- /dev/null
+++ b/app/assets/javascripts/discourse/models/top_list.js.erb
@@ -0,0 +1,37 @@
+/**
+  A data model representing a list of top topic lists
+
+  @class TopList
+  @extends Discourse.Model
+  @namespace Discourse
+  @module Discourse
+**/
+
+Discourse.TopList = Discourse.Model.extend({});
+
+Discourse.TopList.reopenClass({
+  PERIODS: <%= TopTopic.periods.map(&:to_s) %>,
+
+  find: function(period, category) {
+    return PreloadStore.getAndRemove("top_lists", function() {
+      var url = "";
+      if (category) { url += category.get("url") + "/l"; }
+      url += "/top";
+      if (period) { url += "/" + period; }
+      return Discourse.ajax(url + ".json");
+    }).then(function (result) {
+      var topList = Discourse.TopList.create({});
+
+      _.each(Discourse.TopList.PERIODS, function(period) {
+        // if there is a list for that period
+        if (result[period]) {
+          // instanciate a new topic list with no sorting
+          topList.set(period, Discourse.TopicList.from(result[period]));
+          topList.set(period + ".sortOrder", undefined);
+        }
+      });
+
+      return topList;
+    });
+  }
+});
diff --git a/app/assets/javascripts/discourse/models/topic_list.js b/app/assets/javascripts/discourse/models/topic_list.js
index 9c762f264..0eccd3f5a 100644
--- a/app/assets/javascripts/discourse/models/topic_list.js
+++ b/app/assets/javascripts/discourse/models/topic_list.js
@@ -65,8 +65,7 @@ Discourse.TopicList = Discourse.Model.extend({
     params.sort_descending = sortOrder.get('descending');
 
     this.set('loaded', false);
-    var finder = finderFor(this.get('filter'), params);
-    finder().then(function (result) {
+    finderFor(this.get('filter'), params).then(function (result) {
       var newTopics = Discourse.TopicList.topicsFrom(result),
           topics = self.get('topics');
 
diff --git a/app/assets/javascripts/discourse/models/user.js b/app/assets/javascripts/discourse/models/user.js
index c200722b2..1cfb7edb0 100644
--- a/app/assets/javascripts/discourse/models/user.js
+++ b/app/assets/javascripts/discourse/models/user.js
@@ -376,8 +376,6 @@ Discourse.User = Discourse.Model.extend({
 });
 
 Discourse.User.reopenClass(Discourse.Singleton, {
-
-
   /**
     Find a `Discourse.User` for a given username.
 
diff --git a/app/assets/javascripts/discourse/routes/application_routes.js b/app/assets/javascripts/discourse/routes/application_routes.js
index c4cb09a6d..7f6c99a76 100644
--- a/app/assets/javascripts/discourse/routes/application_routes.js
+++ b/app/assets/javascripts/discourse/routes/application_routes.js
@@ -14,34 +14,48 @@ Discourse.Route.buildRoutes(function() {
   });
 
   // Generate static page routes
-  Discourse.StaticController.pages.forEach(function(p) {
-    router.route(p, { path: "/" + p });
+  _.each(Discourse.StaticController.PAGES, function (page) {
+    router.route(page, { path: '/' + page });
   });
 
-  // List routes
-  this.resource('list', { path: '/' }, function() {
+  this.resource("list", { path: "/" }, function() {
     router = this;
 
-    // Generate routes for all our filters
-    Discourse.ListController.filters.forEach(function(filter) {
-      router.route(filter, { path: "/" + filter });
-      router.route(filter, { path: "/" + filter + "/more" });
-      router.route(filter + "Category", { path: "/category/:slug/l/" + filter });
-      router.route(filter + "Category", { path: "/category/:slug/l/" + filter + "/more" });
-      router.route(filter + "Category", { path: "/category/:parentSlug/:slug/l/" + filter });
-      router.route(filter + "Category", { path: "/category/:parentSlug/:slug/l/" + filter + "/more" });
+    // categories
+    this.route('categories');
+
+    // top
+    this.route('top');
+    this.route('topCategory', { path: '/category/:slug/l/top' });
+    this.route('topCategoryNone', { path: '/category/:slug/none/l/top' });
+    this.route('topCategory', { path: '/category/:parentSlug/:slug/l/top' });
+
+    // top by periods
+    _.each(Discourse.TopList.PERIODS, function(period) {
+      var top = 'top' + period.capitalize();
+      router.route(top, { path: '/top/' + period });
+      router.route(top, { path: '/top/' + period + '/more' });
+      router.route(top + 'Category', { path: '/category/:slug/l/top/' + period });
+      router.route(top + 'Category', { path: '/category/:slug/l/top/' + period + '/more' });
+      router.route(top + 'CategoryNone', { path: '/category/:slug/none/l/top/' + period });
+      router.route(top + 'CategoryNone', { path: '/category/:slug/none/l/top/' + period + '/more' });
+      router.route(top + 'Category', { path: '/category/:parentSlug/:slug/l/top/' + period });
+      router.route(top + 'Category', { path: '/category/:parentSlug/:slug/l/top/' + period + '/more' });
     });
 
-    // homepage
-    var homepage = Discourse.User.current() ?
-                   Discourse.User.currentProp("homepage") :
-                   Discourse.Utilities.defaultHomepage();
-    this.route(homepage, { path: '/' });
+    // filters
+    _.each(Discourse.ListController.FILTERS, function(filter) {
+      router.route(filter, { path: '/' + filter });
+      router.route(filter, { path: '/' + filter + '/more' });
+      router.route(filter + 'Category', { path: '/category/:slug/l/' + filter });
+      router.route(filter + 'Category', { path: '/category/:slug/l/' + filter + '/more' });
+      router.route(filter + 'CategoryNone', { path: '/category/:slug/none/l/' + filter });
+      router.route(filter + 'CategoryNone', { path: '/category/:slug/none/l/' + filter + '/more' });
+      router.route(filter + 'Category', { path: '/category/:parentSlug/:slug/l/' + filter });
+      router.route(filter + 'Category', { path: '/category/:parentSlug/:slug/l/' + filter + '/more' });
+    });
 
-    // categories page
-    this.route('categories', { path: '/categories' });
-
-    // category
+    // default filter for a category
     this.route('category', { path: '/category/:slug' });
     this.route('category', { path: '/category/:slug/more' });
     this.route('categoryNone', { path: '/category/:slug/none' });
@@ -49,33 +63,31 @@ Discourse.Route.buildRoutes(function() {
     this.route('category', { path: '/category/:parentSlug/:slug' });
     this.route('category', { path: '/category/:parentSlug/:slug/more' });
 
-    // top page
-    this.route('top', { path: '/top' });
+    // homepage
+    var homepage = Discourse.User.current() ? Discourse.User.currentProp('homepage') : Discourse.Utilities.defaultHomepage();
+    this.route(homepage, { path: '/' });
   });
 
   // User routes
   this.resource('user', { path: '/users/:username' }, function() {
-    this.route('index', { path: '/'} );
-
     this.resource('userActivity', { path: '/activity' }, function() {
-      var self = this;
-      Object.keys(Discourse.UserAction.TYPES).forEach(function (userAction) {
-        self.route(userAction, { path: userAction.replace("_", "-") });
+      router = this;
+      _.map(Discourse.UserAction.TYPES, function (id, userAction) {
+        router.route(userAction, { path: userAction.replace('_', '-') });
       });
     });
 
     this.resource('userPrivateMessages', { path: '/private-messages' }, function() {
-      this.route('mine', { path: '/mine' });
-      this.route('unread', { path: '/unread' });
+      this.route('mine');
+      this.route('unread');
     });
 
-    this.resource('preferences', { path: '/preferences' }, function() {
-      this.route('username', { path: '/username' });
-      this.route('email', { path: '/email' });
+    this.resource('preferences', function() {
+      this.route('username');
+      this.route('email');
       this.route('about', { path: '/about-me' });
-      this.route('avatar', { path: '/avatar' });
     });
 
-    this.route('invited', { path: 'invited' });
+    this.route('invited');
   });
 });
diff --git a/app/assets/javascripts/discourse/routes/discourse_route.js b/app/assets/javascripts/discourse/routes/discourse_route.js
index 4f3be7f70..ee556ba91 100644
--- a/app/assets/javascripts/discourse/routes/discourse_route.js
+++ b/app/assets/javascripts/discourse/routes/discourse_route.js
@@ -32,7 +32,7 @@ Discourse.Route = Em.Route.extend({
     Discourse.set('notifyCount',0);
 
     var hideDropDownFunction = $('html').data('hide-dropdown');
-    if (hideDropDownFunction) return hideDropDownFunction();
+    if (hideDropDownFunction) { hideDropDownFunction(); }
   }
 
 });
diff --git a/app/assets/javascripts/discourse/routes/filtered_list_route.js b/app/assets/javascripts/discourse/routes/filtered_list_route.js
index 408f9ea50..a1d0ec94f 100644
--- a/app/assets/javascripts/discourse/routes/filtered_list_route.js
+++ b/app/assets/javascripts/discourse/routes/filtered_list_route.js
@@ -10,21 +10,8 @@ Discourse.FilteredListRoute = Discourse.Route.extend({
 
   redirect: function() { Discourse.redirectIfLoginRequired(this); },
 
-  deactivate: function() {
-    this._super();
-
-    this.controllerFor('list').setProperties({
-      canCreateTopic: false,
-      filterMode: ''
-    });
-  },
-
   renderTemplate: function() {
-    this.render('listTopics', {
-      into: 'list',
-      outlet: 'listView',
-      controller: 'listTopics'
-    });
+    this.render('listTopics', { into: 'list', outlet: 'listView', controller: 'listTopics' });
   },
 
   setupController: function() {
@@ -44,7 +31,16 @@ Discourse.FilteredListRoute = Discourse.Route.extend({
       listTopicsController.set('model', topicList);
       Discourse.FilteredListRoute.scrollToLastPosition();
     });
-  }
+  },
+
+  deactivate: function() {
+    this._super();
+
+    this.controllerFor('list').setProperties({
+      canCreateTopic: false,
+      filterMode: ''
+    });
+  },
 });
 
 Discourse.FilteredListRoute.reopenClass({
@@ -57,6 +53,10 @@ Discourse.FilteredListRoute.reopenClass({
   }
 });
 
-Discourse.ListController.filters.forEach(function(filter) {
-  Discourse["List" + (filter.capitalize()) + "Route"] = Discourse.FilteredListRoute.extend({ filter: filter });
+_.each(Discourse.ListController.FILTERS, function(filter) {
+  Discourse["List" + filter.capitalize() + "Route"] = Discourse.FilteredListRoute.extend({ filter: filter });
+});
+
+_.each(Discourse.TopList.PERIODS, function(period) {
+  Discourse["ListTop" + period.capitalize() + "Route"] = Discourse.FilteredListRoute.extend({ filter: "top/" + period });
 });
diff --git a/app/assets/javascripts/discourse/routes/list_categories_route.js b/app/assets/javascripts/discourse/routes/list_categories_route.js
index ed8ec0a71..8d3c2f3bc 100644
--- a/app/assets/javascripts/discourse/routes/list_categories_route.js
+++ b/app/assets/javascripts/discourse/routes/list_categories_route.js
@@ -8,34 +8,18 @@
 **/
 Discourse.ListCategoriesRoute = Discourse.Route.extend({
 
-  template: 'listCategories',
-
-  redirect: function() { Discourse.redirectIfLoginRequired(this); },
-
-  actions: {
-    createCategory: function() {
-      Discourse.Route.showModal(this, 'editCategory', Discourse.Category.create({
-        color: 'AB9364', text_color: 'FFFFFF', hotness: 5, group_permissions: [{group_name: "everyone", permission_type: 1}],
-        available_groups: Discourse.Site.current().group_names
-      }));
-      this.controllerFor('editCategory').set('selectedTab', 'general');
-    }
-  },
-
   model: function() {
-    var listTopicsController = this.controllerFor('listTopics');
-    if (listTopicsController) { listTopicsController.set('content', null); }
+    this.controllerFor('listTop').set('content', null);
+    this.controllerFor('listTopics').set('content', null);
     return this.controllerFor('list').load('categories');
   },
 
-  deactivate: function() {
+  activate: function() {
     this._super();
-    this.controllerFor('list').set('canCreateCategory', false);
+    this.controllerFor('list').setProperties({ filterMode: 'categories', category: null });
   },
 
-  renderTemplate: function() {
-    this.render(this.get('template'), { into: 'list', outlet: 'listView' });
-  },
+  redirect: function() { Discourse.redirectIfLoginRequired(this); },
 
   afterModel: function(categoryList) {
     this.controllerFor('list').setProperties({
@@ -44,13 +28,23 @@ Discourse.ListCategoriesRoute = Discourse.Route.extend({
     });
   },
 
-  activate: function() {
-    this.controllerFor('list').setProperties({
-      filterMode: 'categories',
-      category: null
-    });
-  }
+  renderTemplate: function() {
+    this.render('listCategories', { into: 'list', outlet: 'listView' });
+  },
+
+  deactivate: function() {
+    this._super();
+    this.controllerFor('list').set('canCreateCategory', false);
+  },
+
+  actions: {
+    createCategory: function() {
+      Discourse.Route.showModal(this, 'editCategory', Discourse.Category.create({
+        color: 'AB9364', text_color: 'FFFFFF', hotness: 5, group_permissions: [{group_name: 'everyone', permission_type: 1}],
+        available_groups: Discourse.Site.current().group_names
+      }));
+      this.controllerFor('editCategory').set('selectedTab', 'general');
+    }
+  },
 
 });
-
-
diff --git a/app/assets/javascripts/discourse/routes/list_category_route.js b/app/assets/javascripts/discourse/routes/list_category_route.js
index 944535cbd..093378bb4 100644
--- a/app/assets/javascripts/discourse/routes/list_category_route.js
+++ b/app/assets/javascripts/discourse/routes/list_category_route.js
@@ -7,8 +7,17 @@
   @module Discourse
 **/
 Discourse.ListCategoryRoute = Discourse.FilteredListRoute.extend({
+
   model: function(params) {
-    return Discourse.Category.findBySlug(Em.get(params, 'slug'), Em.get(params, 'parentSlug'));
+    this.controllerFor('listTop').set('content', null);
+    this.controllerFor('listCategories').set('content', null);
+    return Discourse.Category.findBySlug(params.slug, params.parentSlug);
+  },
+
+  activate: function() {
+    this._super();
+    // Add a search context
+    this.controllerFor('search').set('searchContext', this.modelFor(this.get('routeName')).get('searchContext'));
   },
 
   setupController: function(controller, category) {
@@ -37,33 +46,29 @@ Discourse.ListCategoryRoute = Discourse.FilteredListRoute.extend({
         canCreateTopic: topicList.get('can_create_topic'),
         category: category
       });
-      self.controllerFor('listTopics').set('content', topicList);
-      self.controllerFor('listTopics').set('category', category);
+      self.controllerFor('listTopics').setProperties({
+        content: topicList,
+        category: category
+      });
       Discourse.FilteredListRoute.scrollToLastPosition();
     });
   },
 
-  activate: function() {
-    this._super();
-
-    // Add a search context
-    this.controllerFor('search').set('searchContext', this.modelFor(this.get('routeName')).get('searchContext'));
-  },
-
   deactivate: function() {
     this._super();
-
     // Clear the search context
     this.controllerFor('search').set('searchContext', null);
   }
 });
 
-Discourse.ListCategoryNoneRoute = Discourse.ListCategoryRoute.extend({
-  noSubcategories: true
-});
+Discourse.ListCategoryNoneRoute = Discourse.ListCategoryRoute.extend({ noSubcategories: true });
 
-Discourse.ListController.filters.forEach(function(filter) {
+_.each(Discourse.ListController.FILTERS, function(filter) {
   Discourse["List" + filter.capitalize() + "CategoryRoute"] = Discourse.ListCategoryRoute.extend({ filter: filter });
+  Discourse["List" + filter.capitalize() + "CategoryNoneRoute"] = Discourse.ListCategoryRoute.extend({ filter: filter, noSubcategories: true });
 });
 
-
+_.each(Discourse.TopList.PERIODS, function(period) {
+  Discourse["ListTop" + period.capitalize() + "CategoryRoute"] = Discourse.ListCategoryRoute.extend({ filter: "top/" + period });
+  Discourse["ListTop" + period.capitalize() + "CategoryNoneRoute"] = Discourse.ListCategoryRoute.extend({ filter: "top/" + period, noSubcategories: true });
+});
diff --git a/app/assets/javascripts/discourse/routes/list_top_route.js b/app/assets/javascripts/discourse/routes/list_top_route.js
index d6a3fb213..7b14f4a70 100644
--- a/app/assets/javascripts/discourse/routes/list_top_route.js
+++ b/app/assets/javascripts/discourse/routes/list_top_route.js
@@ -1,26 +1,42 @@
 Discourse.ListTopRoute = Discourse.Route.extend({
 
-  model: function() {
-    return Discourse.TopList.find();
+  model: function(params) {
+    this.controllerFor('listCategories').set('content', null);
+    this.controllerFor('listTopics').set('content', null);
+    this.controllerFor('list').set('loading', true);
+
+    var category = Discourse.Category.findBySlug(params.slug, params.parentSlug);
+    if (category) { this.set('category', category); }
+
+    return Discourse.TopList.find(this.period, category);
   },
 
   activate: function() {
     this._super();
-    // will mark the "top" navigation item as selected
-    this.controllerFor('list').setProperties({
-      filterMode: 'top',
-      category: null
-    });
+    this.controllerFor('list').setProperties({ filterMode: 'top', category: null });
+  },
+
+  redirect: function() { Discourse.redirectIfLoginRequired(this); },
+
+  setupController: function(controller, model) {
+    var category = this.get('category'),
+        categorySlug = Discourse.Category.slugFor(category),
+        url = category === undefined ? 'top' : 'category/' + categorySlug + '/l/top';
+
+    this.controllerFor('listTop').setProperties({ content: model, category: category });
+    this.controllerFor('list').setProperties({ loading: false, filterMode: url });
+
+    if (category !== undefined) {
+      this.controllerFor('list').set('category', category);
+    }
+
+    Discourse.set('title', I18n.t('filters.top.title'));
   },
 
   renderTemplate: function() {
-    this.render('top', { into: 'list', outlet: 'listView' });
-  },
-
-  deactivate: function() {
-    this._super();
-    // Clear any filters when we leave the route
-    Discourse.URL.set('queryParams', null);
+    this.render('listTop', { into: 'list', outlet: 'listView' });
   }
 
 });
+
+Discourse.ListTopCategoryRoute = Discourse.ListTopRoute.extend({});
diff --git a/app/assets/javascripts/discourse/routes/static_route.js b/app/assets/javascripts/discourse/routes/static_route.js
index dca850d22..2099a2fe3 100644
--- a/app/assets/javascripts/discourse/routes/static_route.js
+++ b/app/assets/javascripts/discourse/routes/static_route.js
@@ -6,16 +6,16 @@
   @namespace Discourse
   @module Discourse
 **/
-Discourse.StaticController.pages.forEach(function(page) {
+_.each(Discourse.StaticController.PAGES, function(page) {
 
-  Discourse[(page.capitalize()) + "Route"] = Discourse.Route.extend({
+  Discourse[page.capitalize() + "Route"] = Discourse.Route.extend({
 
     renderTemplate: function() {
       this.render('static');
     },
 
     setupController: function() {
-      var config_key = Discourse.StaticController.configs[page];
+      var config_key = Discourse.StaticController.CONFIGS[page];
       if (config_key && Discourse.SiteSettings[config_key].length > 0) {
         Discourse.URL.redirectTo(Discourse.SiteSettings[config_key]);
       } else {
@@ -26,5 +26,3 @@ Discourse.StaticController.pages.forEach(function(page) {
   });
 
 });
-
-
diff --git a/app/assets/javascripts/discourse/routes/user_topic_list_routes.js b/app/assets/javascripts/discourse/routes/user_topic_list_routes.js
index 5e55e370c..e55162b88 100644
--- a/app/assets/javascripts/discourse/routes/user_topic_list_routes.js
+++ b/app/assets/javascripts/discourse/routes/user_topic_list_routes.js
@@ -32,7 +32,6 @@ Discourse.UserPrivateMessagesIndexRoute = createPMRoute('index', 'private-messag
 Discourse.UserPrivateMessagesMineRoute = createPMRoute('mine', 'private-messages-sent');
 Discourse.UserPrivateMessagesUnreadRoute = createPMRoute('unread', 'private-messages-unread');
 
-
 Discourse.UserActivityTopicsRoute = Discourse.UserTopicListRoute.extend({
   userActionType: Discourse.UserAction.TYPES.topics,
 
@@ -45,6 +44,6 @@ Discourse.UserActivityStarredRoute = Discourse.UserTopicListRoute.extend({
   userActionType: Discourse.UserAction.TYPES.starred,
 
   model: function() {
-    return Discourse.TopicList.find('starred', {user_id: this.modelFor('user').get('id') });
+    return Discourse.TopicList.find('starred', { user_id: this.modelFor('user').get('id') });
   }
-});
\ No newline at end of file
+});
diff --git a/app/assets/javascripts/discourse/templates/components/basic-topic-list.js.handlebars b/app/assets/javascripts/discourse/templates/components/basic-topic-list.js.handlebars
index a3f35d95c..cb254e0f0 100644
--- a/app/assets/javascripts/discourse/templates/components/basic-topic-list.js.handlebars
+++ b/app/assets/javascripts/discourse/templates/components/basic-topic-list.js.handlebars
@@ -37,9 +37,11 @@
               <a href="{{unbound topic.lastUnreadUrl}}" class='badge new-posts badge-notification' title='{{i18n topic.new}}'><i class='fa fa-asterisk'></i></a>
             {{/if}}
           </td>
+
           <td class="category">
             {{categoryLink topic.category}}
           </td>
+
           <td class='num posts'><a href="{{unbound topic.lastUnreadUrl}}" class='badge-posts'>{{number topic.posts_count numberKey="posts_long"}}</a></td>
 
           <td class='num likes'>
diff --git a/app/assets/javascripts/discourse/templates/list/categories.js.handlebars b/app/assets/javascripts/discourse/templates/list/categories.js.handlebars
index 8ab40b0f5..15c784822 100644
--- a/app/assets/javascripts/discourse/templates/list/categories.js.handlebars
+++ b/app/assets/javascripts/discourse/templates/list/categories.js.handlebars
@@ -1,17 +1,14 @@
 <div class='contents'>
-
   <table id='topic-list' class='categories'>
     <thead>
-    <tr>
-      <th class='category'>{{i18n categories.category}}</th>
-      <th class='latest'>{{i18n categories.latest}}</th>
-      <th class='stats topics'>{{i18n categories.topics}}</th>
-      <th class='stats posts'>{{i18n categories.posts}}
-        {{#if canEdit}}
-          <button title='{{i18n categories.toggle_ordering}}' class='btn toggle-admin no-text' {{action toggleOrdering}}><i class='fa fa-wrench'></i></button>
-        {{/if}}
-      </th>
-    </tr>
+      <tr>
+        <th class='category'>{{i18n categories.category}}</th>
+        <th class='latest'>{{i18n categories.latest}}</th>
+        <th class='stats topics'>{{i18n categories.topics}}</th>
+        <th class='stats posts'>{{i18n categories.posts}}
+          {{#if canEdit}}<button title='{{i18n categories.toggle_ordering}}' class='btn toggle-admin no-text' {{action toggleOrdering}}><i class='fa fa-wrench'></i></button>{{/if}}
+        </th>
+      </tr>
     </thead>
     <tbody>
       {{#each model.categories}}
@@ -19,9 +16,7 @@
         <td class='category'>
           <div>
             <div class="pull-left">
-              {{#if controller.ordering}}
-                <i class="fa fa-bars"></i>
-              {{/if}}
+              {{#if controller.ordering}}<i class="fa fa-bars"></i>{{/if}}
               {{categoryLink this allowUncategorized=true}}
               {{#if unreadTopics}}
                 <a href={{unbound unreadUrl}} class='badge new-posts badge-notification' title='{{i18n topic.unread_topics count="unreadTopics"}}'>{{unbound unreadTopics}}</a>
@@ -42,7 +37,6 @@
               {{{description_excerpt}}}
             </div>
           {{/if}}
-
           {{#if subcategories}}
             <div class='subcategories'>
               {{i18n categories.subcategories}}
@@ -102,10 +96,7 @@
       </tr>
       {{/each}}
     </tbody>
-
   </table>
 </div>
-
-<footer id='topic-list-bottom'>
-</footer>
+<footer id='topic-list-bottom'></footer>
 
diff --git a/app/assets/javascripts/discourse/templates/list/top.js.handlebars b/app/assets/javascripts/discourse/templates/list/top.js.handlebars
new file mode 100644
index 000000000..6d6d08a00
--- /dev/null
+++ b/app/assets/javascripts/discourse/templates/list/top.js.handlebars
@@ -0,0 +1,48 @@
+<div class="top-lists">
+  {{#if redirectedToTopPageReason}}
+    <div class="alert alert-info">
+      {{redirectedToTopPageReason}}
+    </div>
+  {{/if}}
+  {{#if content.yearly}}
+    <div class="clearfix">
+      <h2>{{i18n filters.top.this_year}}</h2>
+      {{basic-topic-list topicList=content.yearly}}
+      {{#if content.yearly.topics.length}}<a href={{unbound showMoreYearlyUrl}} class='btn pull-right'>{{i18n show_more}}</a>{{/if}}
+    </div>
+  {{/if}}
+  {{#if content.monthly}}
+    <div class="clearfix">
+      <h2>{{i18n filters.top.this_month}}</h2>
+      {{basic-topic-list topicList=content.monthly}}
+      {{#if content.monthly.topics.length}}<a href={{unbound showMoreMonthlyUrl}} class='btn pull-right'>{{i18n show_more}}</a>{{/if}}
+    </div>
+  {{/if}}
+  {{#if content.weekly}}
+    <div class="clearfix">
+      <h2>{{i18n filters.top.this_week}}</h2>
+      {{basic-topic-list topicList=content.weekly}}
+      {{#if content.weekly.topics.length}}<a href={{unbound showMoreWeeklyUrl}} class='btn pull-right'>{{i18n show_more}}</a>{{/if}}
+    </div>
+  {{/if}}
+  {{#if content.daily}}
+    <div class="clearfix">
+      <h2>{{i18n filters.top.today}}</h2>
+      {{basic-topic-list topicList=content.daily}}
+      {{#if content.daily.topics.length}}<a href={{unbound showMoreDailyUrl}} class='btn pull-right'>{{i18n show_more}}</a>{{/if}}
+    </div>
+  {{/if}}
+  <footer id="topic-list-bottom">
+    <h3>
+    {{#if hasDisplayedAllTopLists}}
+      {{#link-to "list.categories"}}{{i18n topic.browse_all_categories}}{{/link-to}} {{i18n or}} {{#link-to 'list.latest'}}{{i18n topic.view_latest_topics}}{{/link-to}}.
+    {{else}}
+      {{#link-to "list.categories"}}{{i18n topic.browse_all_categories}}{{/link-to}}, {{#link-to 'list.latest'}}{{i18n topic.view_latest_topics}}{{/link-to}} {{i18n or}} {{i18n filters.top.other_periods}}
+      {{#unless content.yearly}}<a href={{unbound showMoreYearlyUrl}} class='btn'>{{i18n filters.top.this_year}}</a>{{/unless}}
+      {{#unless content.monthly}}<a href={{unbound showMoreMonthlyUrl}} class='btn'>{{i18n filters.top.this_month}}</a>{{/unless}}
+      {{#unless content.weekly}}<a href={{unbound showMoreWeeklyUrl}} class='btn'>{{i18n filters.top.this_week}}</a>{{/unless}}
+      {{#unless content.daily}}<a href={{unbound showMoreDailyUrl}} class='btn'>{{i18n filters.top.today}}</a>{{/unless}}
+    {{/if}}
+    </h3>
+  </footer>
+</div>
diff --git a/app/assets/javascripts/discourse/templates/top.js.handlebars b/app/assets/javascripts/discourse/templates/top.js.handlebars
deleted file mode 100644
index 3c14b0035..000000000
--- a/app/assets/javascripts/discourse/templates/top.js.handlebars
+++ /dev/null
@@ -1,18 +0,0 @@
-{{#if redirectedToTopPageReason}}
-  <div class="alert alert-info">
-    {{redirectedToTopPageReason}}
-  </div>
-{{/if}}
-{{#if showThisYear}}
-  <h2>{{i18n filters.top.this_year}}</h2>
-  {{basic-topic-list topicList=content.yearly}}
-{{/if}}
-<h2>{{i18n filters.top.this_month}}</h2>
-{{basic-topic-list topicList=content.monthly}}
-<h2>{{i18n filters.top.this_week}}</h2>
-{{basic-topic-list topicList=content.weekly}}
-<h2>{{i18n filters.top.today}}</h2>
-{{basic-topic-list topicList=content.daily}}
-<footer id="topic-list-bottom">
-  <h3>{{#link-to "list.categories"}}{{i18n topic.browse_all_categories}}{{/link-to}} {{i18n or}} {{#link-to 'list.latest'}}{{i18n topic.view_latest_topics}}{{/link-to}}</h3>
-</footer>
diff --git a/app/assets/javascripts/discourse/views/list/list_top_view.js b/app/assets/javascripts/discourse/views/list/list_top_view.js
new file mode 100644
index 000000000..6c41f4cca
--- /dev/null
+++ b/app/assets/javascripts/discourse/views/list/list_top_view.js
@@ -0,0 +1,18 @@
+/**
+  This view handles the rendering of the top lists
+
+  @class ListTopView
+  @extends Discourse.View
+  @namespace Discourse
+  @module Discourse
+**/
+Discourse.ListTopView = Discourse.View.extend({
+
+  didInsertElement: function() {
+    this._super();
+    Em.run.schedule('afterRender', function() {
+      $('html, body').scrollTop(0);
+    });
+  },
+
+});
diff --git a/app/assets/javascripts/discourse/views/nav_item_view.js b/app/assets/javascripts/discourse/views/nav_item_view.js
index 8050ac888..7403ac727 100644
--- a/app/assets/javascripts/discourse/views/nav_item_view.js
+++ b/app/assets/javascripts/discourse/views/nav_item_view.js
@@ -25,7 +25,7 @@ Discourse.NavItemView = Discourse.View.extend({
       name = "category";
     }
     return I18n.t("filters." + name + ".help", extra);
-  }.property("content.filter"),
+  }.property("content.name"),
 
 
   name: function() {
diff --git a/app/assets/stylesheets/desktop/topic-list.scss b/app/assets/stylesheets/desktop/topic-list.scss
index 3c392ef27..a83955db0 100644
--- a/app/assets/stylesheets/desktop/topic-list.scss
+++ b/app/assets/stylesheets/desktop/topic-list.scss
@@ -63,6 +63,7 @@
   width: 100%;
   border-collapse: separate;
   border-spacing: 0;
+  margin: 0 0 10px;
   a.title:visited:not(.badge-notification) {color: #888;}
 
   > tbody > tr {
@@ -230,6 +231,7 @@
   }
 }
 
+#list-area .top-lists h2 { margin: 5px 0 10px; }
 
 #topic-list tbody tr.has-excerpt .star {
   vertical-align: top;
diff --git a/app/controllers/list_controller.rb b/app/controllers/list_controller.rb
index f532c550f..3456b474d 100644
--- a/app/controllers/list_controller.rb
+++ b/app/controllers/list_controller.rb
@@ -1,6 +1,15 @@
 class ListController < ApplicationController
 
-  before_filter :ensure_logged_in, except: [:latest, :hot, :category, :top, :category_feed, :latest_feed, :hot_feed, :topics_by]
+  before_filter :ensure_logged_in, except: [
+    :topics_by,
+    # anonymous filters
+    Discourse.anonymous_filters, Discourse.anonymous_filters.map { |f| "#{f}_feed".to_sym },
+    # category
+    :category, :category_feed,
+    # top
+    :top_lists, TopTopic.periods.map { |p| "top_#{p}".to_sym }
+  ].flatten
+
   before_filter :set_category, only: [:category, :category_feed]
   skip_before_filter :check_xhr
 
@@ -72,13 +81,15 @@ class ListController < ApplicationController
     redirect_to latest_path, :status => 301
   end
 
-  def top
+  def top_lists
+    discourse_expires_in 1.minute
+
     top = generate_top_lists
 
     respond_to do |format|
       format.html do
         @top = top
-        store_preloaded('top_list', MultiJson.dump(TopListSerializer.new(top, scope: guardian, root: false)))
+        store_preloaded('top_lists', MultiJson.dump(TopListSerializer.new(top, scope: guardian, root: false)))
         render 'top'
       end
       format.json do
@@ -87,6 +98,16 @@ class ListController < ApplicationController
     end
   end
 
+  TopTopic.periods.each do |period|
+    define_method("top_#{period}") do
+      options = build_topic_list_options
+      user = list_target_user
+      list = TopicQuery.new(user, options).public_send("list_top_#{period}")
+      list.more_topics_url = construct_url_with(period, options, "top")
+      respond(list)
+    end
+  end
+
   protected
 
   def category_response(extra_opts=nil)
@@ -181,19 +202,27 @@ class ListController < ApplicationController
 
   def generate_top_lists
     top = {}
-    topic_ids = Set.new
+    options = {
+      per_page: SiteSetting.topics_per_period_in_summary,
+      category: params[:category]
+    }
+    topic_query = TopicQuery.new(current_user, options)
+    periods = periods_since(current_user.try(:last_seen_at))
 
-    TopTopic.periods.each do |period|
-      options = {
-        per_page: SiteSetting.topics_per_period_in_summary,
-        except_topic_ids: topic_ids.to_a
-      }
-      list = TopicQuery.new(current_user, options).list_top_for(period)
-      topic_ids.merge(list.topic_ids)
-      top[period] = list
-    end
+    periods.each { |period| top[period] = topic_query.list_top_for(period) }
 
     top
   end
 
+  def periods_since(date)
+    date ||= 1.year.ago
+
+    periods = [:daily]
+    periods << :weekly  if date < 8.days.ago
+    periods << :monthly if date < 35.days.ago
+    periods << :yearly  if date < 180.days.ago
+
+    periods
+  end
+
 end
diff --git a/app/serializers/top_list_serializer.rb b/app/serializers/top_list_serializer.rb
index 753830f33..bc6bfd71a 100644
--- a/app/serializers/top_list_serializer.rb
+++ b/app/serializers/top_list_serializer.rb
@@ -1,19 +1,12 @@
 class TopListSerializer < ApplicationSerializer
 
-  attributes :can_create_topic,
-             :yearly,
-             :monthly,
-             :weekly,
-             :daily
-
-  def can_create_topic
-    scope.can_create?(Topic)
-  end
-
   TopTopic.periods.each do |period|
+    attribute period
+
     define_method(period) do
-      TopicListSerializer.new(object[period], scope: scope).as_json
+      TopicListSerializer.new(object[period], scope: scope).as_json if object[period]
     end
+
   end
 
 end
diff --git a/config/locales/client.en.yml b/config/locales/client.en.yml
index 2c58bb1ac..e7bab812e 100644
--- a/config/locales/client.en.yml
+++ b/config/locales/client.en.yml
@@ -604,6 +604,7 @@ en:
         latest: "There are no latest topics. That's sad."
         hot: "There are no hot topics."
         category: "There are no {{category}} topics."
+        top: "There are no top topics."
       bottom:
         latest: "There are no more latest topics."
         hot: "There are no more hot topics."
@@ -613,6 +614,7 @@ en:
         unread: "There are no more unread topics."
         starred: "There are no more starred topics."
         category: "There are no more {{category}} topics."
+        top: "There are no more top topics."
 
     rank_details:
       toggle: toggle topic rank details
@@ -1122,6 +1124,7 @@ en:
         this_month: "This month"
         this_week: "This week"
         today: "Today"
+        other_periods: "dig into other periods"
         redirect_reasons:
           new_user: "Welcome! As a new visitor, we thought you might like to start with this list of the top discussion topics in the last year."
           not_seen_in_a_month: "Welcome back! We haven't seen you in a while. These are the top discussion topics since you've been away."
diff --git a/config/routes.rb b/config/routes.rb
index 7b26573a3..b6e3bea3a 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -106,7 +106,6 @@ Discourse::Application.routes.draw do
   get "email/unsubscribe/:key" => "email#unsubscribe", as: "email_unsubscribe"
   post "email/resubscribe/:key" => "email#resubscribe", as: "email_resubscribe"
 
-
   resources :session, id: USERNAME_ROUTE_FORMAT, only: [:create, :destroy] do
     collection do
       post "forgot_password"
@@ -195,18 +194,33 @@ Discourse::Application.routes.draw do
   end
   resources :user_actions
 
+  # We've renamed popular to latest. If people access it we want a permanent redirect.
+  get "popular" => "list#popular_redirect"
+  get "popular/more" => "list#popular_redirect"
+
   resources :categories, :except => :show
   get "category/:id/show" => "categories#show"
   post "category/:category_id/move" => "categories#move", as: "category_move"
-
   get "category/:category.rss" => "list#category_feed", format: :rss, as: "category_feed"
-  get "category/:category" => "list#category", as: "category_list"
-  get "category/:category/none" => "list#category_none", as: "category_list_none"
-  get "category/:category/more" => "list#category", as: "category_list_more"
+  get "category/:category" => "list#category"
+  get "category/:category/more" => "list#category"
+  get "category/:category/none" => "list#category_none"
+  get "category/:category/none/more" => "list#category_none"
+  get "category/:parent_category/:category" => "list#category"
+  get "category/:parent_category/:category/more" => "list#category"
 
-  # We"ve renamed popular to latest. If people access it we want a permanent redirect.
-  get "popular" => "list#popular_redirect"
-  get "popular/more" => "list#popular_redirect"
+  get "top" => "list#top_lists"
+  get "category/:category/l/top" => "list#top_lists"
+  get "category/:parent_category/:category/l/top" => "list#top_lists"
+
+  TopTopic.periods.each do |period|
+    get "top/#{period}" => "list#top_#{period}"
+    get "top/#{period}/more" => "list#top_#{period}"
+    get "category/:category/l/top/#{period}" => "list#top_#{period}"
+    get "category/:category/l/top/#{period}/more" => "list#top_#{period}"
+    get "category/:parent_category/:category/l/top/#{period}" => "list#top_#{period}"
+    get "category/:parent_category/:category/l/top/#{period}/more" => "list#top_#{period}"
+  end
 
   Discourse.anonymous_filters.each do |filter|
     get "#{filter}.rss" => "list##{filter}_feed", format: :rss
@@ -215,26 +229,19 @@ Discourse::Application.routes.draw do
   Discourse.filters.each do |filter|
     get "#{filter}" => "list##{filter}"
     get "#{filter}/more" => "list##{filter}"
-
     get "category/:category/l/#{filter}" => "list##{filter}"
     get "category/:category/l/#{filter}/more" => "list##{filter}"
     get "category/:parent_category/:category/l/#{filter}" => "list##{filter}"
     get "category/:parent_category/:category/l/#{filter}/more" => "list##{filter}"
   end
 
-  get "top" => "list#top"
-  get "category/:category/l/top" => "list#top"
-  get "category/:parent_category/:category/l/top" => "list#top"
-
-  get "category/:parent_category/:category" => "list#category", as: "category_list_parent"
-
   get "search" => "search#query"
 
   # Topics resource
   get "t/:id" => "topics#show"
-  delete "t/:id" => "topics#destroy"
-  put "t/:id" => "topics#update"
   post "t" => "topics#create"
+  put "t/:id" => "topics#update"
+  delete "t/:id" => "topics#destroy"
   post "topics/timings"
   get "topics/similar_to"
   get "topics/created-by/:username" => "list#topics_by", as: "topics_by", constraints: {username: USERNAME_ROUTE_FORMAT}
@@ -277,7 +284,6 @@ Discourse::Application.routes.draw do
 
   get "raw/:topic_id(/:post_number)" => "posts#markdown"
 
-
   resources :invites
   delete "invites" => "invites#destroy"
 
@@ -299,6 +305,6 @@ Discourse::Application.routes.draw do
   # special case for categories
   root to: "categories#index", constraints: HomePageConstraint.new("categories"), :as => "categories_index"
   # special case for top
-  root to: "list#top", constraints: HomePageConstraint.new("top"), :as => "list_top"
+  root to: "list#top_lists", constraints: HomePageConstraint.new("top"), :as => "top_lists"
 
 end
diff --git a/db/migrate/20131223171005_create_top_topics.rb b/db/migrate/20131223171005_create_top_topics.rb
index 25bda921d..974928c96 100644
--- a/db/migrate/20131223171005_create_top_topics.rb
+++ b/db/migrate/20131223171005_create_top_topics.rb
@@ -1,10 +1,13 @@
 class CreateTopTopics < ActiveRecord::Migration
+  PERIODS = [:yearly, :monthly, :weekly, :daily]
+  SORT_ORDERS = [:posts, :views, :likes]
+
   def change
     create_table :top_topics, force: true do |t|
       t.belongs_to :topic
 
-      TopTopic.periods.each do |period|
-        TopTopic.sort_orders.each do |sort|
+      PERIODS.each do |period|
+        SORT_ORDERS.each do |sort|
           t.integer "#{period}_#{sort}_count".to_sym, null: false, default: 0
         end
       end
@@ -13,8 +16,8 @@ class CreateTopTopics < ActiveRecord::Migration
 
     add_index :top_topics, :topic_id, unique: true
 
-    TopTopic.periods.each do |period|
-      TopTopic.sort_orders.each do |sort|
+    PERIODS.each do |period|
+      SORT_ORDERS.each do |sort|
         add_index :top_topics, "#{period}_#{sort}_count".to_sym, order: 'desc'
       end
     end
diff --git a/lib/topic_query.rb b/lib/topic_query.rb
index 6a3d40e96..81b736aef 100644
--- a/lib/topic_query.rb
+++ b/lib/topic_query.rb
@@ -89,11 +89,17 @@ class TopicQuery
     score = "#{period}_score"
     create_list(:top, unordered: true) do |topics|
       topics.joins(:top_topic)
-            .where("top_topics.#{score} > 1")
+            .where("top_topics.#{score} > 0")
             .order("top_topics.#{score} DESC, topics.bumped_at DESC")
     end
   end
 
+  TopTopic.periods.each do |period|
+    define_method("list_top_#{period}") do
+      list_top_for(period)
+    end
+  end
+
   def list_topics_by(user)
     create_list(:user_topics) do |topics|
       topics.where(user_id: user.id)
diff --git a/test/javascripts/lib/url_test.js b/test/javascripts/lib/url_test.js
index 9b8625bc0..ee8afbae2 100644
--- a/test/javascripts/lib/url_test.js
+++ b/test/javascripts/lib/url_test.js
@@ -8,7 +8,7 @@ test("navigatedToHome", function() {
   mock.expects("refresh").twice();
   ok(Discourse.URL.navigatedToHome("/", "/"));
 
-  var defaultFilter = "/" + Discourse.ListController.filters[0];
+  var defaultFilter = "/" + Discourse.ListController.FILTERS[0];
   ok(Discourse.URL.navigatedToHome(defaultFilter, "/"));
 
   ok(!Discourse.URL.navigatedToHome("/old", "/new"));