Просмотр исходного кода

setting up new frontend for react and redux components

Rafał Pitoń 9 лет назад
Родитель
Сommit
9466657830
35 измененных файлов с 2171 добавлено и 10 удалено
  1. 8 0
      frontend/.babelrc
  2. 4 0
      frontend/.gitignore
  3. 22 0
      frontend/.jshintrc
  4. 152 0
      frontend/gulpfile.js
  5. 40 0
      frontend/package.json
  6. 12 0
      frontend/src/components/user-menu/root.js
  7. 34 0
      frontend/src/index.js
  8. 15 0
      frontend/src/utils/mount-component.js
  9. 117 0
      frontend/src/utils/ordered-list.js
  10. BIN
      frontend/static/fonts/MaterialIcons-Regular.eot
  11. BIN
      frontend/static/fonts/MaterialIcons-Regular.ttf
  12. BIN
      frontend/static/fonts/MaterialIcons-Regular.woff
  13. BIN
      frontend/static/fonts/MaterialIcons-Regular.woff2
  14. 11 0
      frontend/static/fonts/README.md
  15. 795 0
      frontend/static/fonts/codepoints
  16. BIN
      frontend/static/img/site-icon.png
  17. BIN
      frontend/static/img/site-logo.png
  18. 3 0
      frontend/style/flavor/variables.less
  19. 74 0
      frontend/style/index.less
  20. 31 0
      frontend/style/misago/alerts.less
  21. 50 0
      frontend/style/misago/auth-changed-message.less
  22. 45 0
      frontend/style/misago/buttons.less
  23. 89 0
      frontend/style/misago/dropdowns.less
  24. 24 0
      frontend/style/misago/footer.less
  25. 44 0
      frontend/style/misago/forms.less
  26. 158 0
      frontend/style/misago/loaders.less
  27. 42 0
      frontend/style/misago/material-icons.less
  28. 43 0
      frontend/style/misago/message-pages.less
  29. 58 0
      frontend/style/misago/modals.less
  30. 93 0
      frontend/style/misago/navbar.less
  31. 58 0
      frontend/style/misago/variables.less
  32. 83 0
      frontend/tests/misago.js
  33. 59 0
      frontend/tests/ordered-list.js
  34. 4 7
      misago/templates/misago/base.html
  35. 3 3
      misago/templates/misago/navbar.html

+ 8 - 0
frontend/.babelrc

@@ -0,0 +1,8 @@
+{
+  "presets": ["es2015", "stage-2", "react"],
+  "plugins": [
+    ["babel-plugin-module-alias", [
+      { "src": "./src", "expose": "misago" }
+    ]]
+  ]
+}

+ 4 - 0
frontend/.gitignore

@@ -0,0 +1,4 @@
+# dependencies
+/node_modules
+/bower_components
+.cache-require-paths.json

+ 22 - 0
frontend/.jshintrc

@@ -0,0 +1,22 @@
+{
+  "undef": true,
+  "unused": true,
+  "esnext": true,
+  "predef": [
+    "global",
+    "console",
+    "window",
+    "document",
+
+    "gettext",
+    "ngettext",
+    "gettext_noop",
+    "pgettext",
+    "npgettext",
+    "interpolate",
+
+    "beforeEach",
+    "describe",
+    "it"
+  ]
+}

+ 152 - 0
frontend/gulpfile.js

@@ -0,0 +1,152 @@
+'use strict';
+
+require('cache-require-paths');
+
+var gulp = require('gulp');
+
+var babelify = require('babelify');
+var browserify = require('browserify');
+var buffer = require('vinyl-buffer');
+var imageop = require('gulp-image-optimization');
+var jshint = require('gulp-jshint');
+var less = require('gulp-less');
+var minify = require('gulp-minify-css');
+var rename = require('gulp-rename');
+var source = require('vinyl-source-stream');
+var sourcemaps = require('gulp-sourcemaps');
+var uglify = require('gulp-uglify');
+
+var glob = require('glob');
+var del = require('del');
+
+var misago = '../misago/static/misago/';
+
+// Entry points
+
+gulp.task('watch', ['fastbuild'], function() {
+  gulp.watch('src/**/*.js', ['fastsource']);
+  gulp.watch('style/**/*.less', ['faststyle']);
+});
+
+gulp.task('deploy', ['build']);
+
+// Builds
+
+gulp.task('fastbuild', ['fastsource', 'faststyle', 'faststatic']);
+
+gulp.task('build', [
+  'source', 'style', 'static'
+]);
+
+// Source tasks
+
+function getSources() {
+  var sources = ['src/index.js'];
+
+  function include(pattern) {
+    var paths = glob.sync(pattern);
+    paths.forEach(function(path) {
+      sources.push(path);
+    });
+  };
+
+  include('src/initializers/*.js');
+  include('src/components/*.js');
+  include('src/components/**/root.js');
+
+  return sources.map(function(path) {
+    return path;
+  });
+};
+
+gulp.task('lintsource', function() {
+  return gulp.src('src/**/*.js')
+    .pipe(jshint())
+    .pipe(jshint.reporter('default'));
+});
+
+gulp.task('fastsource', ['lintsource'], function() {
+  return browserify(getSources())
+    .transform(babelify)
+    .bundle()
+    .pipe(source('misago.js'))
+    .pipe(buffer())
+    .pipe(gulp.dest(misago + 'js'));
+});
+
+gulp.task('source', ['lintsource'], function() {
+  process.env.NODE_ENV = 'production';
+
+  return browserify(getSources())
+    .transform(babelify)
+    .bundle()
+    .pipe(source('misago.js'))
+    .pipe(buffer())
+    .pipe(sourcemaps.init())
+    .pipe(uglify())
+    .pipe(sourcemaps.write('.'))
+    .pipe(gulp.dest(misago + 'js'));
+});
+
+// Styles tasks
+
+gulp.task('cleanstyle', function(cb) {
+  del(misago + 'css', cb);
+});
+
+gulp.task('faststyle', function() {
+  return gulp.src('style/index.less')
+    .pipe(less())
+    .pipe(rename('misago.css'))
+    .pipe(gulp.dest(misago + 'css'));
+});
+
+gulp.task('style', function() {
+  return gulp.src('style/index.less')
+    .pipe(less())
+    .pipe(minify())
+    .pipe(rename('misago.css'))
+    .pipe(gulp.dest(misago + 'css'));
+});
+
+// Static tasks
+
+gulp.task('copyfonts', function(cb) {
+  return gulp.src('static/fonts/**/*')
+    .pipe(gulp.dest(misago + 'fonts'));
+});
+
+gulp.task('fastcopyimages', function() {
+  return gulp.src('static/img/**/*')
+    .pipe(gulp.dest(misago + 'img'));
+});
+
+gulp.task('copyimages', function() {
+  return gulp.src('static/img/**/*')
+    .pipe(imageop({
+      optimizationLevel: 9
+    }))
+    .pipe(gulp.dest(misago + 'img'));
+});
+
+gulp.task('faststatic', ['copyfonts', 'fastcopyimages']);
+
+gulp.task('static', ['copyfonts', 'copyimages']);
+
+// Vendor tasks
+
+
+// Test task
+
+gulp.task('linttests', function() {
+  return gulp.src(['tests/**/*.js'])
+    .pipe(jshint())
+    .pipe(jshint.reporter('default'));
+});
+
+gulp.task('test', ['linttests', 'lintsource'], function() {
+  var mochify = require('mochify');
+  mochify('tests/**/*.js')
+    .transform(babelify)
+    .bundle();
+});

+ 40 - 0
frontend/package.json

@@ -0,0 +1,40 @@
+{
+  "name": "misago",
+  "version": "0.1.0",
+  "description": "Misago Frontend",
+  "main": "index.js",
+  "scripts": {
+    "test": "gulp test"
+  },
+  "author": "Rafal Piton",
+  "license": "GPL-2.0",
+  "dependencies": {
+    "babel": "^6.3.13",
+    "babel-plugin-module-alias": "^1.0.0",
+    "babel-preset-es2015": "^6.1.18",
+    "babel-preset-react": "^6.1.18",
+    "babel-preset-stage-2": "^6.1.18",
+    "babelify": "^7.2.0",
+    "bower": "^1.6.9",
+    "browserify": "^12.0.1",
+    "cache-require-paths": "^0.1.2",
+    "del": "^2.1.0",
+    "glob": "^6.0.1",
+    "gulp": "^3.9.0",
+    "gulp-image-optimization": "^0.1.3",
+    "gulp-jshint": "^2.0.0",
+    "gulp-less": "^3.0.5",
+    "gulp-minify-css": "^1.2.2",
+    "gulp-rename": "^1.2.2",
+    "gulp-sourcemaps": "^1.6.0",
+    "gulp-uglify": "^1.5.1",
+    "react": "^0.14.3",
+    "react-dom": "^0.14.3",
+    "vinyl-buffer": "^1.0.0",
+    "vinyl-source-stream": "^1.1.0"
+  },
+  "devDependencies": {
+    "jshint": "^2.8.0",
+    "mochify": "^2.14.2"
+  }
+}

+ 12 - 0
frontend/src/components/user-menu/root.js

@@ -0,0 +1,12 @@
+import React from 'react';
+import mount from 'misago/utils/mount-component';
+
+class UserMenu extends React.Component {
+  render() {
+    /* jshint ignore:start */
+    return <p>{'Hello, <b>world</b>!'}</p>;
+    /* jshint ignore:end */
+  }
+}
+
+mount(UserMenu, 'user-menu-mount');

+ 34 - 0
frontend/src/index.js

@@ -0,0 +1,34 @@
+import OrderedList from 'misago/utils/ordered-list';
+
+export class Misago {
+  constructor() {
+    this._initializers = [];
+  }
+
+  addInitializer(initializer) {
+    this._initializers.push({
+      key: initializer.name,
+
+      item: initializer.init,
+
+      after: initializer.after,
+      before: initializer.before
+    });
+  }
+
+  init(options) {
+    var initOrder = new OrderedList(this._initializers).orderedValues();
+    initOrder.forEach(function(initializer) {
+      initializer(options);
+    });
+  }
+}
+
+// create  singleton
+var misago = new Misago();
+
+// expose it globally
+global.misago = misago;
+
+// and export it for tests and stuff
+export default misago;

+ 15 - 0
frontend/src/utils/mount-component.js

@@ -0,0 +1,15 @@
+import React from 'react'; // jshint ignore:line
+import ReactDOM from 'react-dom';
+
+export default function(Component, containerId) {
+  let container = document.getElementById(containerId);
+
+  if (container) {
+    ReactDOM.render(
+      /* jshint ignore:start */
+      <Component/>,
+      /* jshint ignore:end */
+      container
+    );
+  }
+}

+ 117 - 0
frontend/src/utils/ordered-list.js

@@ -0,0 +1,117 @@
+class OrderedList {
+    constructor(items) {
+      this.isOrdered = false;
+      this._items = items || [];
+    }
+
+    add(key, item, order) {
+      this._items.push({
+        key: key,
+        item: item,
+
+        after: order ? order.after || null : null,
+        before: order ? order.before || null : null
+      });
+    }
+
+    get(key, value) {
+      for (var i = 0; i < this._items.length; i++) {
+        if (this._items[i].key === key) {
+          return this._items[i].item;
+        }
+      }
+
+      return value;
+    }
+
+    has(key) {
+      return this.get(key) !== undefined;
+    }
+
+    values() {
+      var values = [];
+      for (var i = 0; i < this._items.length; i++) {
+        values.push(this._items[i].item);
+      }
+      return values;
+    }
+
+    order(values_only) {
+      if (!this.isOrdered) {
+        this._items = this._order(this._items);
+        this.isOrdered = true;
+      }
+
+      if (values_only || typeof values_only === 'undefined') {
+        return this.values();
+      } else {
+        return this._items;
+      }
+    }
+
+    orderedValues() {
+      return this.order(true);
+    }
+
+    _order(unordered) {
+      // Index of unordered items
+      var index = [];
+      unordered.forEach(function (item) {
+        index.push(item.key);
+      });
+
+      // Ordered items
+      var ordered = [];
+      var ordering = [];
+
+      // First pass: register items that
+      // don't specify their order
+      unordered.forEach(function (item) {
+        if (!item.after && !item.before) {
+          ordered.push(item);
+          ordering.push(item.key);
+        }
+      });
+
+      // Second pass: register items that
+      // specify their before to "_end"
+      unordered.forEach(function (item) {
+        if (item.before === "_end") {
+          ordered.push(item);
+          ordering.push(item.key);
+        }
+      });
+
+      // Third pass: keep iterating items
+      // until we hit iterations limit or finish
+      // ordering list
+      function insertItem(item) {
+        var insertAt = -1;
+        if (ordering.indexOf(item.key) === -1) {
+          if (item.after) {
+            insertAt = ordering.indexOf(item.after);
+            if (insertAt !== -1) {
+              insertAt += 1;
+            }
+          } else if (item.before) {
+            insertAt = ordering.indexOf(item.before);
+          }
+
+          if (insertAt !== -1) {
+            ordered.splice(insertAt, 0, item);
+            ordering.splice(insertAt, 0, item.key);
+          }
+        }
+      }
+
+      var iterations = 200;
+      while (iterations > 0 && index.length !== ordering.length) {
+        iterations -= 1;
+        unordered.forEach(insertItem);
+      }
+
+      return ordered;
+    }
+  }
+
+  export default OrderedList;

BIN
frontend/static/fonts/MaterialIcons-Regular.eot


BIN
frontend/static/fonts/MaterialIcons-Regular.ttf


BIN
frontend/static/fonts/MaterialIcons-Regular.woff


BIN
frontend/static/fonts/MaterialIcons-Regular.woff2


+ 11 - 0
frontend/static/fonts/README.md

@@ -0,0 +1,11 @@
+The recommended way to use the Material Icons font is hosted by Google Fonts,
+available here:
+
+```
+<link href="https://fonts.googleapis.com/icon?family=Material+Icons"
+      rel="stylesheet">
+```
+
+Read more in our full usage guide:
+http://google.github.io/material-design-icons/#icon-font-for-the-web
+

+ 795 - 0
frontend/static/fonts/codepoints

@@ -0,0 +1,795 @@
+3d_rotation e84d
+access_alarm e190
+access_alarms e191
+access_time e192
+accessibility e84e
+account_balance e84f
+account_balance_wallet e850
+account_box e851
+account_circle e853
+adb e60e
+add e145
+add_alarm e193
+add_alert e003
+add_box e146
+add_circle e147
+add_circle_outline e148
+add_shopping_cart e854
+add_to_photos e39d
+adjust e39e
+airline_seat_flat e630
+airline_seat_flat_angled e631
+airline_seat_individual_suite e632
+airline_seat_legroom_extra e633
+airline_seat_legroom_normal e634
+airline_seat_legroom_reduced e635
+airline_seat_recline_extra e636
+airline_seat_recline_normal e637
+airplanemode_active e195
+airplanemode_inactive e194
+airplay e055
+alarm e855
+alarm_add e856
+alarm_off e857
+alarm_on e858
+album e019
+android e859
+announcement e85a
+apps e5c3
+archive e149
+arrow_back e5c4
+arrow_drop_down e5c5
+arrow_drop_down_circle e5c6
+arrow_drop_up e5c7
+arrow_forward e5c8
+aspect_ratio e85b
+assessment e85c
+assignment e85d
+assignment_ind e85e
+assignment_late e85f
+assignment_return e860
+assignment_returned e861
+assignment_turned_in e862
+assistant e39f
+assistant_photo e3a0
+attach_file e226
+attach_money e227
+attachment e2bc
+audiotrack e3a1
+autorenew e863
+av_timer e01b
+backspace e14a
+backup e864
+battery_alert e19c
+battery_charging_full e1a3
+battery_full e1a4
+battery_std e1a5
+battery_unknown e1a6
+beenhere e52d
+block e14b
+bluetooth e1a7
+bluetooth_audio e60f
+bluetooth_connected e1a8
+bluetooth_disabled e1a9
+bluetooth_searching e1aa
+blur_circular e3a2
+blur_linear e3a3
+blur_off e3a4
+blur_on e3a5
+book e865
+bookmark e866
+bookmark_border e867
+border_all e228
+border_bottom e229
+border_clear e22a
+border_color e22b
+border_horizontal e22c
+border_inner e22d
+border_left e22e
+border_outer e22f
+border_right e230
+border_style e231
+border_top e232
+border_vertical e233
+brightness_1 e3a6
+brightness_2 e3a7
+brightness_3 e3a8
+brightness_4 e3a9
+brightness_5 e3aa
+brightness_6 e3ab
+brightness_7 e3ac
+brightness_auto e1ab
+brightness_high e1ac
+brightness_low e1ad
+brightness_medium e1ae
+broken_image e3ad
+brush e3ae
+bug_report e868
+build e869
+business e0af
+cached e86a
+cake e7e9
+call e0b0
+call_end e0b1
+call_made e0b2
+call_merge e0b3
+call_missed e0b4
+call_received e0b5
+call_split e0b6
+camera e3af
+camera_alt e3b0
+camera_enhance e8fc
+camera_front e3b1
+camera_rear e3b2
+camera_roll e3b3
+cancel e5c9
+card_giftcard e8f6
+card_membership e8f7
+card_travel e8f8
+cast e307
+cast_connected e308
+center_focus_strong e3b4
+center_focus_weak e3b5
+change_history e86b
+chat e0b7
+chat_bubble e0ca
+chat_bubble_outline e0cb
+check e5ca
+check_box e834
+check_box_outline_blank e835
+check_circle e86c
+chevron_left e5cb
+chevron_right e5cc
+chrome_reader_mode e86d
+class e86e
+clear e14c
+clear_all e0b8
+close e5cd
+closed_caption e01c
+cloud e2bd
+cloud_circle e2be
+cloud_done e2bf
+cloud_download e2c0
+cloud_off e2c1
+cloud_queue e2c2
+cloud_upload e2c3
+code e86f
+collections e3b6
+collections_bookmark e431
+color_lens e3b7
+colorize e3b8
+comment e0b9
+compare e3b9
+computer e30a
+confirmation_number e638
+contact_phone e0cf
+contacts e0ba
+content_copy e14d
+content_cut e14e
+content_paste e14f
+control_point e3ba
+control_point_duplicate e3bb
+create e150
+credit_card e870
+crop e3be
+crop_16_9 e3bc
+crop_3_2 e3bd
+crop_5_4 e3bf
+crop_7_5 e3c0
+crop_din e3c1
+crop_free e3c2
+crop_landscape e3c3
+crop_original e3c4
+crop_portrait e3c5
+crop_square e3c6
+dashboard e871
+data_usage e1af
+dehaze e3c7
+delete e872
+description e873
+desktop_mac e30b
+desktop_windows e30c
+details e3c8
+developer_board e30d
+developer_mode e1b0
+device_hub e335
+devices e1b1
+dialer_sip e0bb
+dialpad e0bc
+directions e52e
+directions_bike e52f
+directions_boat e532
+directions_bus e530
+directions_car e531
+directions_railway e534
+directions_run e566
+directions_subway e533
+directions_transit e535
+directions_walk e536
+disc_full e610
+dns e875
+do_not_disturb e612
+do_not_disturb_alt e611
+dock e30e
+domain e7ee
+done e876
+done_all e877
+drafts e151
+drive_eta e613
+dvr e1b2
+edit e3c9
+eject e8fb
+email e0be
+equalizer e01d
+error e000
+error_outline e001
+event e878
+event_available e614
+event_busy e615
+event_note e616
+event_seat e903
+exit_to_app e879
+expand_less e5ce
+expand_more e5cf
+explicit e01e
+explore e87a
+exposure e3ca
+exposure_neg_1 e3cb
+exposure_neg_2 e3cc
+exposure_plus_1 e3cd
+exposure_plus_2 e3ce
+exposure_zero e3cf
+extension e87b
+face e87c
+fast_forward e01f
+fast_rewind e020
+favorite e87d
+favorite_border e87e
+feedback e87f
+file_download e2c4
+file_upload e2c6
+filter e3d3
+filter_1 e3d0
+filter_2 e3d1
+filter_3 e3d2
+filter_4 e3d4
+filter_5 e3d5
+filter_6 e3d6
+filter_7 e3d7
+filter_8 e3d8
+filter_9 e3d9
+filter_9_plus e3da
+filter_b_and_w e3db
+filter_center_focus e3dc
+filter_drama e3dd
+filter_frames e3de
+filter_hdr e3df
+filter_list e152
+filter_none e3e0
+filter_tilt_shift e3e2
+filter_vintage e3e3
+find_in_page e880
+find_replace e881
+flag e153
+flare e3e4
+flash_auto e3e5
+flash_off e3e6
+flash_on e3e7
+flight e539
+flight_land e904
+flight_takeoff e905
+flip e3e8
+flip_to_back e882
+flip_to_front e883
+folder e2c7
+folder_open e2c8
+folder_shared e2c9
+folder_special e617
+font_download e167
+format_align_center e234
+format_align_justify e235
+format_align_left e236
+format_align_right e237
+format_bold e238
+format_clear e239
+format_color_fill e23a
+format_color_reset e23b
+format_color_text e23c
+format_indent_decrease e23d
+format_indent_increase e23e
+format_italic e23f
+format_line_spacing e240
+format_list_bulleted e241
+format_list_numbered e242
+format_paint e243
+format_quote e244
+format_size e245
+format_strikethrough e246
+format_textdirection_l_to_r e247
+format_textdirection_r_to_l e248
+format_underlined e249
+forum e0bf
+forward e154
+forward_10 e056
+forward_30 e057
+forward_5 e058
+fullscreen e5d0
+fullscreen_exit e5d1
+functions e24a
+gamepad e30f
+games e021
+gesture e155
+get_app e884
+gif e908
+gps_fixed e1b3
+gps_not_fixed e1b4
+gps_off e1b5
+grade e885
+gradient e3e9
+grain e3ea
+graphic_eq e1b8
+grid_off e3eb
+grid_on e3ec
+group e7ef
+group_add e7f0
+group_work e886
+hd e052
+hdr_off e3ed
+hdr_on e3ee
+hdr_strong e3f1
+hdr_weak e3f2
+headset e310
+headset_mic e311
+healing e3f3
+hearing e023
+help e887
+help_outline e8fd
+high_quality e024
+highlight_off e888
+history e889
+home e88a
+hotel e53a
+hourglass_empty e88b
+hourglass_full e88c
+http e902
+https e88d
+image e3f4
+image_aspect_ratio e3f5
+import_export e0c3
+inbox e156
+indeterminate_check_box e909
+info e88e
+info_outline e88f
+input e890
+insert_chart e24b
+insert_comment e24c
+insert_drive_file e24d
+insert_emoticon e24e
+insert_invitation e24f
+insert_link e250
+insert_photo e251
+invert_colors e891
+invert_colors_off e0c4
+iso e3f6
+keyboard e312
+keyboard_arrow_down e313
+keyboard_arrow_left e314
+keyboard_arrow_right e315
+keyboard_arrow_up e316
+keyboard_backspace e317
+keyboard_capslock e318
+keyboard_hide e31a
+keyboard_return e31b
+keyboard_tab e31c
+keyboard_voice e31d
+label e892
+label_outline e893
+landscape e3f7
+language e894
+laptop e31e
+laptop_chromebook e31f
+laptop_mac e320
+laptop_windows e321
+launch e895
+layers e53b
+layers_clear e53c
+leak_add e3f8
+leak_remove e3f9
+lens e3fa
+library_add e02e
+library_books e02f
+library_music e030
+link e157
+list e896
+live_help e0c6
+live_tv e639
+local_activity e53f
+local_airport e53d
+local_atm e53e
+local_bar e540
+local_cafe e541
+local_car_wash e542
+local_convenience_store e543
+local_dining e556
+local_drink e544
+local_florist e545
+local_gas_station e546
+local_grocery_store e547
+local_hospital e548
+local_hotel e549
+local_laundry_service e54a
+local_library e54b
+local_mall e54c
+local_movies e54d
+local_offer e54e
+local_parking e54f
+local_pharmacy e550
+local_phone e551
+local_pizza e552
+local_play e553
+local_post_office e554
+local_printshop e555
+local_see e557
+local_shipping e558
+local_taxi e559
+location_city e7f1
+location_disabled e1b6
+location_off e0c7
+location_on e0c8
+location_searching e1b7
+lock e897
+lock_open e898
+lock_outline e899
+looks e3fc
+looks_3 e3fb
+looks_4 e3fd
+looks_5 e3fe
+looks_6 e3ff
+looks_one e400
+looks_two e401
+loop e028
+loupe e402
+loyalty e89a
+mail e158
+map e55b
+markunread e159
+markunread_mailbox e89b
+memory e322
+menu e5d2
+merge_type e252
+message e0c9
+mic e029
+mic_none e02a
+mic_off e02b
+mms e618
+mode_comment e253
+mode_edit e254
+money_off e25c
+monochrome_photos e403
+mood e7f2
+mood_bad e7f3
+more e619
+more_horiz e5d3
+more_vert e5d4
+mouse e323
+movie e02c
+movie_creation e404
+music_note e405
+my_location e55c
+nature e406
+nature_people e407
+navigate_before e408
+navigate_next e409
+navigation e55d
+network_cell e1b9
+network_locked e61a
+network_wifi e1ba
+new_releases e031
+nfc e1bb
+no_sim e0cc
+not_interested e033
+note_add e89c
+notifications e7f4
+notifications_active e7f7
+notifications_none e7f5
+notifications_off e7f6
+notifications_paused e7f8
+offline_pin e90a
+ondemand_video e63a
+open_in_browser e89d
+open_in_new e89e
+open_with e89f
+pages e7f9
+pageview e8a0
+palette e40a
+panorama e40b
+panorama_fish_eye e40c
+panorama_horizontal e40d
+panorama_vertical e40e
+panorama_wide_angle e40f
+party_mode e7fa
+pause e034
+pause_circle_filled e035
+pause_circle_outline e036
+payment e8a1
+people e7fb
+people_outline e7fc
+perm_camera_mic e8a2
+perm_contact_calendar e8a3
+perm_data_setting e8a4
+perm_device_information e8a5
+perm_identity e8a6
+perm_media e8a7
+perm_phone_msg e8a8
+perm_scan_wifi e8a9
+person e7fd
+person_add e7fe
+person_outline e7ff
+person_pin e55a
+personal_video e63b
+phone e0cd
+phone_android e324
+phone_bluetooth_speaker e61b
+phone_forwarded e61c
+phone_in_talk e61d
+phone_iphone e325
+phone_locked e61e
+phone_missed e61f
+phone_paused e620
+phonelink e326
+phonelink_erase e0db
+phonelink_lock e0dc
+phonelink_off e327
+phonelink_ring e0dd
+phonelink_setup e0de
+photo e410
+photo_album e411
+photo_camera e412
+photo_library e413
+photo_size_select_actual e432
+photo_size_select_large e433
+photo_size_select_small e434
+picture_as_pdf e415
+picture_in_picture e8aa
+pin_drop e55e
+place e55f
+play_arrow e037
+play_circle_filled e038
+play_circle_outline e039
+play_for_work e906
+playlist_add e03b
+plus_one e800
+poll e801
+polymer e8ab
+portable_wifi_off e0ce
+portrait e416
+power e63c
+power_input e336
+power_settings_new e8ac
+present_to_all e0df
+print e8ad
+public e80b
+publish e255
+query_builder e8ae
+question_answer e8af
+queue e03c
+queue_music e03d
+radio e03e
+radio_button_checked e837
+radio_button_unchecked e836
+rate_review e560
+receipt e8b0
+recent_actors e03f
+redeem e8b1
+redo e15a
+refresh e5d5
+remove e15b
+remove_circle e15c
+remove_circle_outline e15d
+remove_red_eye e417
+reorder e8fe
+repeat e040
+repeat_one e041
+replay e042
+replay_10 e059
+replay_30 e05a
+replay_5 e05b
+reply e15e
+reply_all e15f
+report e160
+report_problem e8b2
+restaurant_menu e561
+restore e8b3
+ring_volume e0d1
+room e8b4
+rotate_90_degrees_ccw e418
+rotate_left e419
+rotate_right e41a
+router e328
+satellite e562
+save e161
+scanner e329
+schedule e8b5
+school e80c
+screen_lock_landscape e1be
+screen_lock_portrait e1bf
+screen_lock_rotation e1c0
+screen_rotation e1c1
+sd_card e623
+sd_storage e1c2
+search e8b6
+security e32a
+select_all e162
+send e163
+settings e8b8
+settings_applications e8b9
+settings_backup_restore e8ba
+settings_bluetooth e8bb
+settings_brightness e8bd
+settings_cell e8bc
+settings_ethernet e8be
+settings_input_antenna e8bf
+settings_input_component e8c0
+settings_input_composite e8c1
+settings_input_hdmi e8c2
+settings_input_svideo e8c3
+settings_overscan e8c4
+settings_phone e8c5
+settings_power e8c6
+settings_remote e8c7
+settings_system_daydream e1c3
+settings_voice e8c8
+share e80d
+shop e8c9
+shop_two e8ca
+shopping_basket e8cb
+shopping_cart e8cc
+shuffle e043
+signal_cellular_4_bar e1c8
+signal_cellular_connected_no_internet_4_bar e1cd
+signal_cellular_no_sim e1ce
+signal_cellular_null e1cf
+signal_cellular_off e1d0
+signal_wifi_4_bar e1d8
+signal_wifi_4_bar_lock e1d9
+signal_wifi_off e1da
+sim_card e32b
+sim_card_alert e624
+skip_next e044
+skip_previous e045
+slideshow e41b
+smartphone e32c
+sms e625
+sms_failed e626
+snooze e046
+sort e164
+sort_by_alpha e053
+space_bar e256
+speaker e32d
+speaker_group e32e
+speaker_notes e8cd
+speaker_phone e0d2
+spellcheck e8ce
+star e838
+star_border e83a
+star_half e839
+stars e8d0
+stay_current_landscape e0d3
+stay_current_portrait e0d4
+stay_primary_landscape e0d5
+stay_primary_portrait e0d6
+stop e047
+storage e1db
+store e8d1
+store_mall_directory e563
+straighten e41c
+strikethrough_s e257
+style e41d
+subject e8d2
+subtitles e048
+supervisor_account e8d3
+surround_sound e049
+swap_calls e0d7
+swap_horiz e8d4
+swap_vert e8d5
+swap_vertical_circle e8d6
+switch_camera e41e
+switch_video e41f
+sync e627
+sync_disabled e628
+sync_problem e629
+system_update e62a
+system_update_alt e8d7
+tab e8d8
+tab_unselected e8d9
+tablet e32f
+tablet_android e330
+tablet_mac e331
+tag_faces e420
+tap_and_play e62b
+terrain e564
+text_format e165
+textsms e0d8
+texture e421
+theaters e8da
+thumb_down e8db
+thumb_up e8dc
+thumbs_up_down e8dd
+time_to_leave e62c
+timelapse e422
+timer e425
+timer_10 e423
+timer_3 e424
+timer_off e426
+toc e8de
+today e8df
+toll e8e0
+tonality e427
+toys e332
+track_changes e8e1
+traffic e565
+transform e428
+translate e8e2
+trending_down e8e3
+trending_flat e8e4
+trending_up e8e5
+tune e429
+turned_in e8e6
+turned_in_not e8e7
+tv e333
+undo e166
+unfold_less e5d6
+unfold_more e5d7
+usb e1e0
+verified_user e8e8
+vertical_align_bottom e258
+vertical_align_center e259
+vertical_align_top e25a
+vibration e62d
+video_library e04a
+videocam e04b
+videocam_off e04c
+view_agenda e8e9
+view_array e8ea
+view_carousel e8eb
+view_column e8ec
+view_comfy e42a
+view_compact e42b
+view_day e8ed
+view_headline e8ee
+view_list e8ef
+view_module e8f0
+view_quilt e8f1
+view_stream e8f2
+view_week e8f3
+vignette e435
+visibility e8f4
+visibility_off e8f5
+voice_chat e62e
+voicemail e0d9
+volume_down e04d
+volume_mute e04e
+volume_off e04f
+volume_up e050
+vpn_key e0da
+vpn_lock e62f
+wallpaper e1bc
+warning e002
+watch e334
+wb_auto e42c
+wb_cloudy e42d
+wb_incandescent e42e
+wb_iridescent e436
+wb_sunny e430
+wc e63d
+web e051
+whatshot e80e
+widgets e1bd
+wifi e63e
+wifi_lock e1e1
+wifi_tethering e1e2
+work e8f9
+wrap_text e25b
+youtube_searched_for e8fa
+zoom_in e8ff
+zoom_out e900

BIN
frontend/static/img/site-icon.png


BIN
frontend/static/img/site-logo.png


+ 3 - 0
frontend/style/flavor/variables.less

@@ -0,0 +1,3 @@
+//
+// Flavor Theme Variables
+// --------------------------------------------------

+ 74 - 0
frontend/style/index.less

@@ -0,0 +1,74 @@
+//
+// Misago Theme Bootstrap
+// --------------------------------------------------
+
+// Path to Boostrap's LESS
+@bs: "../bower_components/bootstrap/less/";
+
+// Core variables
+@import "@{bs}variables.less";
+@import "misago/variables.less";
+@import "flavor/variables.less";
+
+
+// --------------------------------------------------
+// Bootstrap
+// --------------------------------------------------
+
+// Core variables and mixins
+@import "@{bs}mixins.less";
+
+// Reset and dependencies
+@import "@{bs}normalize.less";
+@import "@{bs}print.less";
+
+// Core CSS
+@import "@{bs}scaffolding.less";
+@import "@{bs}type.less";
+@import "@{bs}code.less";
+@import "@{bs}grid.less";
+@import "@{bs}tables.less";
+@import "@{bs}forms.less";
+@import "@{bs}buttons.less";
+
+// Components
+@import "@{bs}component-animations.less";
+@import "@{bs}dropdowns.less";
+@import "@{bs}navs.less";
+@import "@{bs}navbar.less";
+@import "@{bs}alerts.less";
+@import "@{bs}progress-bars.less";
+@import "@{bs}media.less";
+@import "@{bs}wells.less";
+@import "@{bs}close.less";
+
+// Components w/ JavaScript
+@import "@{bs}modals.less";
+
+// Utility classes
+@import "@{bs}utilities.less";
+@import "@{bs}responsive-utilities.less";
+
+
+// --------------------------------------------------
+// Misago
+// --------------------------------------------------
+
+// Components
+@import "misago/auth-changed-message.less";
+@import "misago/alerts.less";
+@import "misago/loaders.less";
+@import "misago/navbar.less";
+@import "misago/material-icons.less";
+@import "misago/modals.less";
+@import "misago/forms.less";
+@import "misago/buttons.less";
+@import "misago/dropdowns.less";
+@import "misago/footer.less";
+
+// Pages
+@import "misago/message-pages.less";
+
+// --------------------------------------------------
+// Flavor
+// --------------------------------------------------

+ 31 - 0
frontend/style/misago/alerts.less

@@ -0,0 +1,31 @@
+//
+// Alerts
+// --------------------------------------------------
+
+
+.alerts {
+  position: fixed;
+  top: -100%;
+  width: 100%;
+  z-index: @zindex-modal + 10;
+
+  text-align: center;
+  font-size: @font-size-large;
+
+  transition: top 300ms ease;
+
+  pointer-events: none;
+
+  &.in {
+    top: 0px;
+    transition: top 200ms ease;
+  }
+
+  p {
+    display: inline-block;
+    border-radius: 0px 0px @border-radius-base @border-radius-base;
+    margin: 0px;
+
+    pointer-events: all;
+  }
+}

+ 50 - 0
frontend/style/misago/auth-changed-message.less

@@ -0,0 +1,50 @@
+//
+// Auth changed message
+// --------------------------------------------------
+
+
+.auth-changed-message {
+  width: 100%;
+
+  position: fixed;
+  top: 0px;
+  left: 0px;
+
+  z-index: @zindex-auth-message;
+
+  transition: top 300ms ease;
+
+  &>div {
+    position: absolute;
+    bottom: 0px;
+    width: 100%;
+
+    background-color: @auth-changed-bg;
+    padding: @line-height-computed 0px;
+  }
+
+  &.show > div {
+    top: 0px;
+    bottom: auto;
+  }
+
+  p {
+    padding: @line-height-computed / 2 0px;
+
+    color: @auth-changed-color;
+    font-size: @font-size-large;
+  }
+}
+
+// Small displays
+@media screen and (max-width: @screen-sm-max) {
+  .auth-changed-message {
+    text-align: center;
+
+    .btn {
+      padding: @padding-large-vertical @padding-large-horizontal;
+
+      font-size: @font-size-large;
+    }
+  }
+}

+ 45 - 0
frontend/style/misago/buttons.less

@@ -0,0 +1,45 @@
+//
+// Buttons
+// --------------------------------------------------
+
+
+// Button loading style
+.btn.btn-loading {
+  &, &:link, &:active, &:visited, &:hover, &:focus {
+    // make text transparent, but still take place in button
+    color: transparent;
+
+    // position loader over the text
+    .loader-compact {
+      margin-top: @line-height-computed * -1;
+      height: 20px;
+
+      position: relative;
+      top: 0px;
+    }
+  }
+
+  &.btn-default .loader-compact > div {
+    background-color: @btn-default-color;
+  }
+
+  &.btn-primary .loader-compact > div {
+    background-color: @btn-primary-color;
+  }
+
+  &.btn-success .loader-compact > div {
+    background-color: @btn-success-color;
+  }
+
+  &.btn-info .loader-compact > div {
+    background-color: @btn-info-color;
+  }
+
+  &.btn-warning .loader-compact > div {
+    background-color: @btn-warning-color;
+  }
+
+  &.btn-danger .loader-compact > div {
+    background-color: @btn-danger-color;
+  }
+}

+ 89 - 0
frontend/style/misago/dropdowns.less

@@ -0,0 +1,89 @@
+//
+// Dropdowns
+// --------------------------------------------------
+
+
+// Unify .btn-link appearance with anchors
+.dropdown-menu>li>a,
+.dropdown-menu>li>.btn-link {
+  display: block;
+  border: none;
+  clear: both;
+  float: none;
+  padding: 4px 20px;
+  width: 100%;
+
+  color: @dropdown-link-color;
+  font-weight: normal;
+  line-height: @line-height-base;
+  text-align: left;
+  white-space: nowrap;
+
+  &:hover, &:focus {
+    background-color: @dropdown-link-hover-bg;
+
+    color: @dropdown-link-hover-color;
+    text-decoration: none;
+  }
+
+  // Set material icons in dropdown menus
+  .material-icon {
+    margin: -2px 0px;
+    margin-right: @line-height-computed * .35;
+
+    position: relative;
+    bottom: 1px;
+
+    font-size: 18px;
+  }
+}
+
+// Dropdown footer
+.dropdown-menu .dropdown-footer {
+  padding: 6px 20px;
+}
+
+
+// Always displayed on mobile dropdown
+.mobile-dropdown {
+  position: static;
+  margin: 0px;
+}
+
+.mobile-dropdown.open>.dropdown-menu {
+  border: none;
+  border-radius: 0;
+  .box-shadow(none);
+
+  display: block;
+  margin: 0px;
+  width: 100%;
+
+  position: static;
+}
+
+
+// Guest menu
+.user-dropdown .guest-preview {
+  text-align: center;
+
+  .row {
+    margin: 0px;
+  }
+}
+
+
+// User menu
+.navbar .user-dropdown {
+  width: 240px;
+}
+
+.user-dropdown .dropdown-header {
+  padding: 6px 20px;
+
+  font-size: @font-size-large;
+
+  strong {
+    font-weight: normal;
+  }
+}

+ 24 - 0
frontend/style/misago/footer.less

@@ -0,0 +1,24 @@
+//
+// Forum Footer style
+// --------------------------------------------------
+
+
+// Superbasic spacing and layout for easy overriding
+.misago-footer {
+  margin-top: @line-height-computed * 1.5;
+  margin-bottom: @line-height-computed * 2.5;
+
+  .footer-content {
+    border-top: 1px solid @page-header-border-color;
+    padding-top: @line-height-computed * 1.5;
+  }
+}
+
+
+// Position "enable JS!" message's icon
+.misago-footer .noscript-message .material-icon {
+  position: relative;
+  bottom: 1px;
+
+  font-size: @font-size-large;
+}

+ 44 - 0
frontend/style/misago/forms.less

@@ -0,0 +1,44 @@
+//
+// Forms
+// --------------------------------------------------
+
+// Material feedback icon
+.material-icon.form-control-feedback {
+  top: @padding-base-vertical;
+  right: @padding-base-horizontal * 2;
+
+  font-size: @line-height-base;
+  line-height: @line-height-base;
+}
+
+
+// Well done
+.well.well-form.well-done {
+  font-size: @font-size-large;
+  text-align: center;
+
+  .message-icon {
+    margin-bottom: @line-height-computed / 2;
+
+    font-size: @font-size-large * 5;
+    line-height: @font-size-large * 5;
+  }
+
+  .message-body {
+    margin-bottom: @line-height-computed / 3;
+  }
+}
+
+
+// Noscript well
+.well.well-form.well-noscript {
+  font-size: @font-size-large;
+  text-align: center;
+
+  .message-icon {
+    margin-bottom: @line-height-computed / 2;
+
+    font-size: @font-size-large * 5;
+    line-height: @font-size-large * 5;
+  }
+}

+ 158 - 0
frontend/style/misago/loaders.less

@@ -0,0 +1,158 @@
+//
+// Misago Loaders
+// --------------------------------------------------
+
+
+.loader {
+  &.sk-folding-cube {
+    margin: @loader-vertical-margin auto;
+    width: @loader-size;
+    height: @loader-size;
+    position: relative;
+    -webkit-transform: rotateZ(45deg);
+            transform: rotateZ(45deg);
+  }
+
+  &.sk-folding-cube .sk-cube {
+    float: left;
+    width: 50%;
+    height: 50%;
+    position: relative;
+    -webkit-transform: scale(1.1);
+        -ms-transform: scale(1.1);
+            transform: scale(1.1);
+  }
+
+  &.sk-folding-cube .sk-cube:before {
+    content: '';
+    position: absolute;
+    top: 0;
+    left: 0;
+    width: 100%;
+    height: 100%;
+    background-color: @loader-color;
+    -webkit-animation: sk-foldCubeAngle 2.4s infinite linear both;
+            animation: sk-foldCubeAngle 2.4s infinite linear both;
+    -webkit-transform-origin: 100% 100%;
+        -ms-transform-origin: 100% 100%;
+            transform-origin: 100% 100%;
+  }
+
+  &.sk-folding-cube .sk-cube2 {
+    -webkit-transform: scale(1.1) rotateZ(90deg);
+            transform: scale(1.1) rotateZ(90deg);
+  }
+
+  &.sk-folding-cube .sk-cube3 {
+    -webkit-transform: scale(1.1) rotateZ(180deg);
+            transform: scale(1.1) rotateZ(180deg);
+  }
+
+  &.sk-folding-cube .sk-cube4 {
+    -webkit-transform: scale(1.1) rotateZ(270deg);
+            transform: scale(1.1) rotateZ(270deg);
+  }
+
+  &.sk-folding-cube .sk-cube2:before {
+    -webkit-animation-delay: 0.3s;
+            animation-delay: 0.3s;
+  }
+
+  &.sk-folding-cube .sk-cube3:before {
+    -webkit-animation-delay: 0.6s;
+            animation-delay: 0.6s;
+  }
+
+  &.sk-folding-cube .sk-cube4:before {
+    -webkit-animation-delay: 0.9s;
+            animation-delay: 0.9s;
+  }
+
+  @-webkit-keyframes sk-foldCubeAngle {
+    0%, 10% {
+      -webkit-transform: perspective(140px) rotateX(-180deg);
+              transform: perspective(140px) rotateX(-180deg);
+      opacity: 0;
+    } 25%, 75% {
+      -webkit-transform: perspective(140px) rotateX(0deg);
+              transform: perspective(140px) rotateX(0deg);
+      opacity: 1;
+    } 90%, 100% {
+      -webkit-transform: perspective(140px) rotateY(180deg);
+              transform: perspective(140px) rotateY(180deg);
+      opacity: 0;
+    }
+  }
+
+  @keyframes sk-foldCubeAngle {
+    0%, 10% {
+      -webkit-transform: perspective(140px) rotateX(-180deg);
+              transform: perspective(140px) rotateX(-180deg);
+      opacity: 0;
+    } 25%, 75% {
+      -webkit-transform: perspective(140px) rotateX(0deg);
+              transform: perspective(140px) rotateX(0deg);
+      opacity: 1;
+    } 90%, 100% {
+      -webkit-transform: perspective(140px) rotateY(180deg);
+              transform: perspective(140px) rotateY(180deg);
+      opacity: 0;
+    }
+  }
+}
+
+
+.loader-compact {
+  margin: 0px auto;
+  width: 50px;
+  text-align: center;
+
+  &>div {
+    width: @loader-compact-height;
+    height: @loader-compact-height;
+    background-color: #333;
+
+    border-radius: 100%;
+    display: inline-block;
+    -webkit-animation: sk-bouncedelay 1.4s infinite ease-in-out both;
+    animation: sk-bouncedelay 1.4s infinite ease-in-out both;
+  }
+
+  .bounce1 {
+    -webkit-animation-delay: -0.32s;
+    animation-delay: -0.32s;
+  }
+
+  .bounce2 {
+    margin: 0px 4px;
+    -webkit-animation-delay: -0.16s;
+    animation-delay: -0.16s;
+  }
+
+  @-webkit-keyframes sk-bouncedelay {
+    0%, 80%, 100% { -webkit-transform: scale(0.5) }
+    40% { -webkit-transform: scale(1.0) }
+  }
+
+  @keyframes sk-bouncedelay {
+    0%, 80%, 100% {
+      -webkit-transform: scale(0.5);
+      transform: scale(0.5);
+    } 40% {
+      -webkit-transform: scale(1.0);
+      transform: scale(1.0);
+    }
+  }
+}
+
+
+// Loading page extra styles
+.page-loading {
+  .lead {
+    margin-top: @loader-vertical-margin * -0.5;
+    margin-bottom: @loader-vertical-margin;
+
+    color: @loader-text;
+    text-align: center;
+  }
+}

+ 42 - 0
frontend/style/misago/material-icons.less

@@ -0,0 +1,42 @@
+//
+// Material Icons
+// --------------------------------------------------
+
+
+@font-face {
+  font-family: 'Material Icons';
+  font-style: normal;
+  font-weight: 400;
+  src: url(../fonts/MaterialIcons-Regular.eot); /* For IE6-8 */
+  src: local('Material Icons'),
+       local('MaterialIcons-Regular'),
+       url(../fonts/MaterialIcons-Regular.woff2) format('woff2'),
+       url(../fonts/MaterialIcons-Regular.woff) format('woff'),
+       url(../fonts/MaterialIcons-Regular.ttf) format('truetype');
+}
+
+
+.material-icon {
+  font-family: 'Material Icons';
+  font-weight: normal;
+  font-style: normal;
+  display: inline-block;
+  width: 1em;
+  height: 1em;
+  line-height: 1;
+  text-align: center;
+  text-transform: none;
+  letter-spacing: normal;
+  vertical-align: middle;
+
+  /* Support for all WebKit browsers. */
+  -webkit-font-smoothing: antialiased;
+  /* Support for Safari and Chrome. */
+  text-rendering: optimizeLegibility;
+
+  /* Support for Firefox. */
+  -moz-osx-font-smoothing: grayscale;
+
+  /* Support for IE. */
+  font-feature-settings: 'liga';
+}

+ 43 - 0
frontend/style/misago/message-pages.less

@@ -0,0 +1,43 @@
+//
+// Message Pages
+// --------------------------------------------------
+
+
+// Small displays
+@media screen and (max-width: @screen-sm-max) {
+  .page-message, .page-error {
+    text-align: center;
+
+    .message-icon {
+      margin: @line-height-computed * 1.5;
+
+      .material-icon {
+        font-size: @message-page-icon-size * 2;
+      }
+    }
+  }
+}
+
+
+// Full displays
+@media screen and (min-width: @screen-md-min) {
+  .page-message, .page-error {
+    .message-panel {
+      margin: @line-height-computed * 3 auto;
+      max-width: @screen-md-max * .65;
+      overflow: auto;
+    }
+
+    .message-icon {
+      float: left;
+
+      .material-icon {
+        font-size: @message-page-icon-size;
+      }
+    }
+
+    .message-body {
+      margin-left: @message-page-icon-size + @line-height-computed;
+    }
+  }
+}

+ 58 - 0
frontend/style/misago/modals.less

@@ -0,0 +1,58 @@
+//
+// Misago Modals
+// --------------------------------------------------
+
+
+// Modals displaying messages
+
+// Small displays
+@media screen and (max-width: @screen-sm-max) {
+  .modal-message {
+    text-align: center;
+
+    .message-icon {
+      margin: @line-height-computed * 1.5;
+
+      .material-icon {
+        font-size: @message-page-icon-size * 2;
+      }
+    }
+  }
+}
+
+
+// Full displays
+@media screen and (min-width: @screen-md-min) {
+  .modal-message {
+    .message-icon {
+      float: left;
+
+      .material-icon {
+        font-size: @message-page-icon-size;
+      }
+    }
+
+    .message-body {
+      margin-left: @message-page-icon-size + @line-height-computed;
+    }
+  }
+}
+
+
+// Registration modal
+@media screen and (max-width: @screen-sm-max) {
+  .modal-register .modal-footer {
+    text-align: center;
+
+    a {
+      display: block;
+    }
+
+    .btn {
+      display: block;
+      float: none;
+      margin-top: @line-height-computed / 2;
+      width: 100%;
+    }
+  }
+}

+ 93 - 0
frontend/style/misago/navbar.less

@@ -0,0 +1,93 @@
+//
+// Misago Navbar
+// --------------------------------------------------
+
+
+// Remove bottom margin from navbar
+.navbar {
+  margin-bottom: 0px;
+}
+
+// Desktop navbar
+.navbar .navbar-full {
+  // Brand
+  .navbar-brand {
+    &>* {
+      display: inline-block;
+    }
+
+    img {
+      height: 16px;
+    }
+  }
+
+  // Pull Guest and User menus to right
+  .nav-guest, .nav-user {
+    float: right;
+
+    .navbar-btn {
+      margin-left: @navbar-padding-horizontal;
+    }
+  }
+
+  // User avatar size
+  .nav-user .dropdown-toggle {
+    padding: (@navbar-height - @navbar-avatar-size) / 2;
+
+    img {
+      width: @navbar-avatar-size;
+      height: @navbar-avatar-size;
+    }
+  }
+}
+
+
+// Compact (mobile) navbar
+.navbar ul.navbar-compact-nav {
+  margin: 0px;
+  display: table;
+  width: 100%;
+
+  &>li {
+    display: table-cell;
+  }
+
+  &>li>a, &>li>button {
+    background: none;
+    border: none;
+    margin: 0px;
+    padding-top: 10px;
+    padding-bottom: 10px;
+    width: 100%;
+
+    color: @navbar-default-link-color;
+    text-align: center;
+
+    &:hover, &:focus {
+      color: @navbar-default-link-hover-color;
+      background-color: @navbar-default-link-hover-bg;
+    }
+
+    &>img {
+      width: @navbar-compact-item-size;
+      height: @navbar-compact-item-size;
+    }
+  }
+
+  &>li>button {
+    padding-top:    10px;
+    padding-bottom: 10px;
+  }
+
+  &>li>a>.material-icon {
+    font-size: @navbar-compact-item-size;
+    line-height: @navbar-compact-item-size;
+  }
+}
+
+// Make navbar's height match compact nav
+@media (max-width: (@grid-float-breakpoint - 1)) {
+  .navbar.navbar-misago {
+    min-height: auto;
+  }
+}

+ 58 - 0
frontend/style/misago/variables.less

@@ -0,0 +1,58 @@
+//
+// Misago Default Theme Variables
+// --------------------------------------------------
+
+
+//== Loaders
+//
+//## Loaders appearance and spacing.
+
+//** Full loader vertical margin
+@loader-vertical-margin: 80px;
+//** Full loader size (both width and height)
+@loader-size: 80px;
+//** Loader color
+@loader-color: @gray-light;
+//** Loader text color
+@loader-text: @gray-light;
+
+//** Compact loader dot height
+@loader-compact-height: 10px;
+
+
+//== Navbar
+//
+//## Extra navbar configurability
+
+//** Full navbar avatar padding
+@navbar-avatar-size:         34px;
+
+//** Compact navbar
+@navbar-compact-item-size:   24px;
+
+//== Error pages
+//
+@message-page-icon-size: 80px;
+
+
+//== Auth Changed message
+//
+@auth-changed-bg:         @gray-lighter;
+@auth-changed-color:      @gray-darker;
+
+
+//-- Z-index master list
+//
+// Warning: Avoid customizing these values. They're used for a bird's eye view
+// of components dependent on the z-axis and are designed to all work together.
+//
+// Note: These variables are not generated into the Customizer.
+
+@zindex-navbar:            1000;
+@zindex-dropdown:          1000;
+@zindex-popover:           1060;
+@zindex-tooltip:           1070;
+@zindex-navbar-fixed:      1030;
+@zindex-modal-background:  1040;
+@zindex-modal:             1050;
+@zindex-auth-message:      1070;

+ 83 - 0
frontend/tests/misago.js

@@ -0,0 +1,83 @@
+import assert from 'assert';
+import { Misago } from 'misago/index';
+
+var misago = null;
+
+describe('Misago', function() {
+  it("addInitializer registers new initializer", function() {
+    misago = new Misago();
+
+    misago.addInitializer({
+      name: 'test',
+      init: null
+    });
+
+    assert.equal(misago._initializers[0].key, 'test',
+      "test initializer was registered");
+
+    assert.equal(misago._initializers.length, 1,
+      "addInitializer() registered single initializer in container");
+    assert.equal(misago._initializers[0].key, 'test',
+      "addInitializer() registered test initializer in container");
+  });
+
+  it("init() calls test initializer", function() {
+    misago = new Misago();
+
+    misago.addInitializer({
+      name: 'test',
+      init: function(options) {
+        assert.equal(options, 'tru', "initializer was called with options");
+      }
+    });
+
+    misago.init('tru');
+  });
+
+  it("init() calls test initializers in order", function() {
+    misago = new Misago();
+
+    misago.addInitializer({
+      name: 'carrot',
+      init: function(options) {
+        assert.equal(options.next, 'carrot',
+          "first initializer was called in right order");
+
+        options.next = 'apple';
+      },
+      before: 'apple'
+    });
+
+    misago.addInitializer({
+      name: 'apple',
+      init: function(options) {
+        assert.equal(options.next, 'apple',
+          "second initializer was called in right order");
+
+        options.next = 'orange';
+      }
+    });
+
+    misago.addInitializer({
+      name: 'orange',
+      init: function(options) {
+        assert.equal(options.next, 'orange',
+          "pen-ultimate initializer was called in right order");
+
+        options.next = 'banana';
+      },
+      before: '_end'
+    });
+
+    misago.addInitializer({
+      name: 'banana',
+      init: function(options) {
+        assert.equal(options.next, 'banana',
+          "ultimate initializer was called in right order");
+      },
+      after: 'orange'
+    });
+
+    misago.init({next: 'carrot'});
+  });
+});

+ 59 - 0
frontend/tests/ordered-list.js

@@ -0,0 +1,59 @@
+import assert from 'assert';
+import OrderedList from 'misago/utils/ordered-list';
+
+var list = null;
+
+describe('OrderedList', function() {
+  beforeEach(function() {
+    list = new OrderedList();
+  });
+
+  it("stores and returns items", function () {
+    assert.equal(list.has('invalid_key'), false,
+      "list.has() returned false for nonexisting key");
+
+    list.add('valid_key', 'valid_value');
+    assert.ok(list.has('valid_key'),
+      "list.has() returned true for existing key");
+    assert.equal(list.get('valid_key'), 'valid_value',
+      "list.get() returned value for existing key");
+
+    assert.equal(list.get('invalid_key', 'fallback'), 'fallback',
+      "list.get() returned fallback value for nonexisting key");
+  });
+
+  it("orders and returns items", function() {
+    list.add('apple', 'apple');
+    list.add('banana', 'banana');
+    list.add('orange', 'orange');
+    list.add('lichi', 'lichi', {before: '_end'});
+    list.add('kiwi', 'kiwi', {before: 'banana'});
+    list.add('melon', 'melon', {after: 'potato'});
+    list.add('potato', 'potato');
+
+    var unorderedList = list.values();
+
+    assert.equal(list.isOrdered, false,
+      "list.values() didn't set isOrdered flag on list");
+
+    assert.equal(unorderedList[0], 'apple', 'unorderedList[0] is apple');
+    assert.equal(unorderedList[1], 'banana', 'unorderedList[1] is banana');
+    assert.equal(unorderedList[2], 'orange', 'unorderedList[2] is orange');
+    assert.equal(unorderedList[3], 'lichi', 'unorderedList[3] is lichi');
+    assert.equal(unorderedList[4], 'kiwi', 'unorderedList[4] is kiwi');
+    assert.equal(unorderedList[5], 'melon', 'unorderedList[5] is melon');
+    assert.equal(unorderedList[6], 'potato', 'unorderedList[6] is potato');
+
+    var orderedList = list.order();
+
+    assert.ok(list.isOrdered, "list.order() set isOrdered flag on list");
+
+    assert.equal(orderedList[0], 'apple', 'orderedList[0] is apple');
+    assert.equal(orderedList[1], 'kiwi', 'orderedList[1] is kiwi');
+    assert.equal(orderedList[2], 'banana', 'orderedList[2] is banana');
+    assert.equal(orderedList[3], 'orange', 'orderedList[3] is orange');
+    assert.equal(orderedList[4], 'potato', 'orderedList[4] is potato');
+    assert.equal(orderedList[5], 'melon', 'orderedList[5] is melon');
+    assert.equal(orderedList[6], 'lichi', 'orderedList[6] is lichi');
+  });
+});

+ 4 - 7
misago/templates/misago/base.html

@@ -24,8 +24,8 @@
   </head>
   <body>
 
-    <div id="auth-changed-message-mount" data-component-name="auth-changed-message"></div>
-    <div id="alert-mount" data-component-name="alert"></div>
+    <div id="auth-changed-message-mount"></div>
+    <div id="alert-mount"></div>
 
     {% include "misago/jumbotron.html" %}
     {% include "misago/navbar.html" %}
@@ -40,17 +40,14 @@
     <div class="modal fade" id="modal-mount" tabindex="-1" role="dialog" aria-labelledby="misago-modal-label"></div>
 
     <script type="text/javascript" src="/django-i18n.js"></script>
-    <script type="text/javascript" src="{% static 'misago/js/vendor.js' %}"></script>
     {% if LANGUAGE_CODE != "en-us" %}
     <script type="text/javascript" src="/moment-i18n.js"></script>
     {% endif %}
+    <script type="text/javascript" src="{% static 'misago/js/vendor.js' %}"></script>
     <script type="text/javascript" src="{% static 'misago/js/misago.js' %}"></script>
     {% include "misago/scripts.html" %}
     <script type="text/javascript">
-      var misago = new Misago();
-      misago.init({
-        api: '{% url 'misago:api:api-root' %}'
-      }, {{ frontend_context|as_json }});
+      misago.init({{ frontend_context|as_json }});
     </script>
 
   </body>

+ 3 - 3
misago/templates/misago/navbar.html

@@ -34,7 +34,7 @@
       </li>
     </ul>
 
-    <div id="user-menu-mount" data-component-name="navbar:desktop:{{ user.is_authenticated|yesno:'user,guest' }}-nav"></div>
+    <div id="user-menu-mount"></div>
   </div><!-- /full navbar -->
 
   <ul class="nav navbar-nav navbar-compact-nav hidden-md hidden-lg">
@@ -60,8 +60,8 @@
         <i class="material-icon">search</i>
       </a>
     </li>
-    <li id="user-menu-compact-mount" data-component-name="navbar:compact:{{ user.is_authenticated|yesno:'user,guest' }}-nav"></li>
+    <li id="user-menu-compact-mount"></li>
   </ul>
   <!-- /compact navbar -->
 </nav>
-<div id="navbar-dropdown" class="mobile-dropdown"></div>
+<div id="navbar-dropdown-mount" class="mobile-dropdown"></div>