Browse Source

Q3Stats is now open source! :)

Tomek Wójcik 2 years ago
commit
bfdcb87cef
100 changed files with 9455 additions and 0 deletions
  1. 14 0
      .gitignore
  2. 19 0
      LICENSE
  3. 13 0
      MANIFEST.in
  4. 182 0
      README.rst
  5. 1 0
      alembic/README
  6. 72 0
      alembic/env.py
  7. 24 0
      alembic/script.py.mako
  8. 35 0
      alembic/versions/32ecefbbf8d3_create_table_games.py
  9. 44 0
      alembic/versions/3509d93df7c_create_table_scores.py
  10. 53 0
      frontend/.eslintrc.json
  11. 7 0
      frontend/.gitignore
  12. 143 0
      frontend/Gruntfile.js
  13. 37 0
      frontend/error.html
  14. BIN
      frontend/fonts/glyphicons-halflings-regular.eot
  15. 288 0
      frontend/fonts/glyphicons-halflings-regular.svg
  16. BIN
      frontend/fonts/glyphicons-halflings-regular.ttf
  17. BIN
      frontend/fonts/glyphicons-halflings-regular.woff
  18. BIN
      frontend/fonts/glyphicons-halflings-regular.woff2
  19. 0 0
      frontend/img/items/.placeholder
  20. 0 0
      frontend/img/powerups/.placeholder
  21. 0 0
      frontend/img/weapons/.placeholder
  22. 34 0
      frontend/index.html
  23. 38 0
      frontend/package.json
  24. 36 0
      frontend/sass/_mixins.scss
  25. 20 0
      frontend/sass/components/ChartComponent.scss
  26. 21 0
      frontend/sass/components/LoaderComponent.scss
  27. 43 0
      frontend/sass/main.scss
  28. 56 0
      frontend/sass/vendor/_bootstrap.scss
  29. 73 0
      frontend/sass/vendor/bootstrap/_alerts.scss
  30. 68 0
      frontend/sass/vendor/bootstrap/_badges.scss
  31. 28 0
      frontend/sass/vendor/bootstrap/_breadcrumbs.scss
  32. 244 0
      frontend/sass/vendor/bootstrap/_button-groups.scss
  33. 168 0
      frontend/sass/vendor/bootstrap/_buttons.scss
  34. 270 0
      frontend/sass/vendor/bootstrap/_carousel.scss
  35. 36 0
      frontend/sass/vendor/bootstrap/_close.scss
  36. 69 0
      frontend/sass/vendor/bootstrap/_code.scss
  37. 37 0
      frontend/sass/vendor/bootstrap/_component-animations.scss
  38. 216 0
      frontend/sass/vendor/bootstrap/_dropdowns.scss
  39. 617 0
      frontend/sass/vendor/bootstrap/_forms.scss
  40. 307 0
      frontend/sass/vendor/bootstrap/_glyphicons.scss
  41. 84 0
      frontend/sass/vendor/bootstrap/_grid.scss
  42. 171 0
      frontend/sass/vendor/bootstrap/_input-groups.scss
  43. 54 0
      frontend/sass/vendor/bootstrap/_jumbotron.scss
  44. 66 0
      frontend/sass/vendor/bootstrap/_labels.scss
  45. 130 0
      frontend/sass/vendor/bootstrap/_list-group.scss
  46. 66 0
      frontend/sass/vendor/bootstrap/_media.scss
  47. 40 0
      frontend/sass/vendor/bootstrap/_mixins.scss
  48. 150 0
      frontend/sass/vendor/bootstrap/_modals.scss
  49. 662 0
      frontend/sass/vendor/bootstrap/_navbar.scss
  50. 242 0
      frontend/sass/vendor/bootstrap/_navs.scss
  51. 424 0
      frontend/sass/vendor/bootstrap/_normalize.scss
  52. 54 0
      frontend/sass/vendor/bootstrap/_pager.scss
  53. 89 0
      frontend/sass/vendor/bootstrap/_pagination.scss
  54. 271 0
      frontend/sass/vendor/bootstrap/_panels.scss
  55. 131 0
      frontend/sass/vendor/bootstrap/_popovers.scss
  56. 101 0
      frontend/sass/vendor/bootstrap/_print.scss
  57. 87 0
      frontend/sass/vendor/bootstrap/_progress-bars.scss
  58. 35 0
      frontend/sass/vendor/bootstrap/_responsive-embed.scss
  59. 179 0
      frontend/sass/vendor/bootstrap/_responsive-utilities.scss
  60. 161 0
      frontend/sass/vendor/bootstrap/_scaffolding.scss
  61. 234 0
      frontend/sass/vendor/bootstrap/_tables.scss
  62. 291 0
      frontend/sass/vendor/bootstrap/_theme.scss
  63. 38 0
      frontend/sass/vendor/bootstrap/_thumbnails.scss
  64. 101 0
      frontend/sass/vendor/bootstrap/_tooltip.scss
  65. 298 0
      frontend/sass/vendor/bootstrap/_type.scss
  66. 55 0
      frontend/sass/vendor/bootstrap/_utilities.scss
  67. 874 0
      frontend/sass/vendor/bootstrap/_variables.scss
  68. 29 0
      frontend/sass/vendor/bootstrap/_wells.scss
  69. 14 0
      frontend/sass/vendor/bootstrap/mixins/_alerts.scss
  70. 12 0
      frontend/sass/vendor/bootstrap/mixins/_background-variant.scss
  71. 18 0
      frontend/sass/vendor/bootstrap/mixins/_border-radius.scss
  72. 65 0
      frontend/sass/vendor/bootstrap/mixins/_buttons.scss
  73. 7 0
      frontend/sass/vendor/bootstrap/mixins/_center-block.scss
  74. 22 0
      frontend/sass/vendor/bootstrap/mixins/_clearfix.scss
  75. 88 0
      frontend/sass/vendor/bootstrap/mixins/_forms.scss
  76. 58 0
      frontend/sass/vendor/bootstrap/mixins/_gradients.scss
  77. 81 0
      frontend/sass/vendor/bootstrap/mixins/_grid-framework.scss
  78. 122 0
      frontend/sass/vendor/bootstrap/mixins/_grid.scss
  79. 21 0
      frontend/sass/vendor/bootstrap/mixins/_hide-text.scss
  80. 33 0
      frontend/sass/vendor/bootstrap/mixins/_image.scss
  81. 12 0
      frontend/sass/vendor/bootstrap/mixins/_labels.scss
  82. 32 0
      frontend/sass/vendor/bootstrap/mixins/_list-group.scss
  83. 10 0
      frontend/sass/vendor/bootstrap/mixins/_nav-divider.scss
  84. 9 0
      frontend/sass/vendor/bootstrap/mixins/_nav-vertical-align.scss
  85. 8 0
      frontend/sass/vendor/bootstrap/mixins/_opacity.scss
  86. 24 0
      frontend/sass/vendor/bootstrap/mixins/_pagination.scss
  87. 24 0
      frontend/sass/vendor/bootstrap/mixins/_panels.scss
  88. 10 0
      frontend/sass/vendor/bootstrap/mixins/_progress-bar.scss
  89. 8 0
      frontend/sass/vendor/bootstrap/mixins/_reset-filter.scss
  90. 18 0
      frontend/sass/vendor/bootstrap/mixins/_reset-text.scss
  91. 6 0
      frontend/sass/vendor/bootstrap/mixins/_resize.scss
  92. 21 0
      frontend/sass/vendor/bootstrap/mixins/_responsive-visibility.scss
  93. 10 0
      frontend/sass/vendor/bootstrap/mixins/_size.scss
  94. 9 0
      frontend/sass/vendor/bootstrap/mixins/_tab-focus.scss
  95. 28 0
      frontend/sass/vendor/bootstrap/mixins/_table-row.scss
  96. 12 0
      frontend/sass/vendor/bootstrap/mixins/_text-emphasis.scss
  97. 8 0
      frontend/sass/vendor/bootstrap/mixins/_text-overflow.scss
  98. 222 0
      frontend/sass/vendor/bootstrap/mixins/_vendor-prefixes.scss
  99. 13 0
      frontend/sass/views/AboutModalView.scss
  100. 0 0
      frontend/sass/views/ErrorView.scss

+ 14 - 0
.gitignore

@@ -0,0 +1,14 @@
+.DS_Store
+*.bak
+*.pyc
+*.pyo
+*.swp
+
+/*.cfg
+/alembic.ini
+
+build/
+dist/
+q3stats.egg-info/
+
+tests_web_app/tmp/

+ 19 - 0
LICENSE

@@ -0,0 +1,19 @@
+Copyright (c) 2017 Tomek Wójcik <tomek@bthlabs.pl>
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.

+ 13 - 0
MANIFEST.in

@@ -0,0 +1,13 @@
+include LICENSE
+include requirements.txt
+include README.rst
+include tox.ini
+recursive-include q3stats/skel *
+global-exclude .gitignore
+global-exclude .placeholder
+recursive-include q3stats/web_app/static *.min.css
+recursive-include q3stats/web_app/static *.min.js
+recursive-include q3stats/web_app/static/fonts glyphicons*
+recursive-include q3stats/web_app/static/fonts glyphicons*
+recursive-include q3stats/web_app/static/img *.gif *.png
+recursive-include q3stats/web_app/templates *.html

+ 182 - 0
README.rst

@@ -0,0 +1,182 @@
+Q3Stats
+=======
+
+Stats for your CPMA server.
+
+About
+-----
+
+Q3Stats is a Web application that allows you to import and analyze stats
+generated by CPMA mod for Quake 3: Arena.
+
+Features
+--------
+
+* Import game stats from XML files generated by CPMA,
+* View dashboard with stats of the last session and the current month,
+* View detailed session stats,
+* View detailed stats of player's performance in a game,
+* View aggregated player stats for sessions and maps.
+
+Requirements
+------------
+
+* Unix-like OS (tested on OS X and Debian 8),
+* PostgreSQL 9.5+,
+* Development environment (compilers, headers, etc),
+* Python 2.7 or 3.5+ with development headers,
+* lxml2 with development headers,
+* libxslt with development headers,
+* libpq with development headers,
+* SASS and Compass,
+* imagemagick, wget and unzip for icons extraction script.
+
+Project setup
+-------------
+
+To set up Q3Stats for please follow the guide below:
+
+#. Install system requirements
+#. Create a virtualenv: ``$ virtualenv q3stats``
+#. Enter the virtualenv: ``$ cd q3stats && source bin/activate``
+#. Clone the repository:
+   ``(q3stats)$ git clone https://git.bthlabs.pl/tomekwojcik/q3stats.git``
+#. Create the Alembic configuration file:
+   ``(q3stats)$ cp q3stats/skel/alembic.ini .``
+#. Edit the Alembic configuration file to suit your needs
+#. Install the backend app requirements:
+   ``(q3stats)$ pip install -r requirements-dev.txt``
+#. Set up the frontend app: ``(q3stats)$ cd frontend && npm install``
+
+**Extracting icons for items, weapons and powerups from CPMA resources**
+
+To extract icons from CPMA PK3 file, use the following command:
+
+.. sourcecode:: shell
+
+  (q3stats)$ python utils/extract_icons.py <path_to_z-cpma-pak148.pk3>
+
+If you omit the argument, the script will download CPMA and use the downloaded
+file to extract required icons.
+
+The extracted icons will be converted to PNG and placed in proper paths.
+
+Developing Q3Stats
+------------------
+
+To set up Q3Stats for development please follow the additional steps:
+
+#. Create the backend app configuration file:
+   ``(q3stats)$ cp q3stats/skel/example.cfg development.cfg``
+#. Create the testing configuration file:
+   ``(q3stats)$ cp q3stats/skel/example.cfg testing.cfg``
+#. Edit the configuration files to suit your needs
+#. Run the database migrations: ``(q3stats)$ alembic upgrade head``
+
+**Working with backend app**
+
+You can develop the backend app like any other Flask Web app. To start the
+development server, use the following command:
+
+.. sourcecode:: shell
+
+  (q3stats)$ python run_web_server.py
+
+To run the test suite, use the following command:
+
+.. sourcecode:: shell
+
+  (q3stats)$ nosetests
+
+**Working with frontend app**
+
+The frontend app uses Grunt for building and development.
+
+Use the following command to build development version of the app:
+
+.. sourcecode:: shell
+
+  (q3stats)frontend$ grunt dev
+
+The built assets won't be minified and will contain proper source maps.
+
+In development, it's handy to watch source files for changes. Q3Stats' build
+system supports watching two classes of sources - JavaScript and SASS.
+
+To start watching JavaScript sources for changes, use the following command:
+
+.. sourcecode:: shell
+
+  (q3stats)frontend$ grunt watch:js
+
+To start watching SASS sources for changes, use the following command:
+
+.. sourcecode:: shell
+
+  (q3stats)frontend$ grunt watch:js
+
+Production setup
+----------------
+
+To set up Q3Stats for production use please follow the additional steps:
+
+#. Build production version of frontend app:
+   ``(q3stats)frontend$ grunt dist``
+#. Build sdist package: ``(q3stats)$ python setup.py sdist``
+#. Generate database migration script:
+   ``(q3stats)$ alembic upgrade --sql head >./prod_migration.sql``
+#. Transfer the sdist package and migration script to your server,
+#. Set up environment on the server according to requirements and your needs,
+#. Install the sdist package,
+#. Migrate database using the migration script,
+#. (Re)start the application.
+
+**Configuration file in production**
+
+In order for Q3Stats app to start up it needs the configuration file. You can
+use ``Q3STATS_CONFIG_FILE`` environment variable to pass path to your
+configuration file.
+
+**Tips on migrating the production database**
+
+Alembic's *upgrade* command can be used to generate migration script
+either for empty database (like in the guide above) or for upgrade of existing
+database.
+
+To generate upgrade script, use the following command:
+
+.. sourcecode:: shell
+
+  (q3stats)$ alembic upgrade --sql <version_num>:head> >./prod_upgrade.sql
+
+You can obtain the *version_num* by running the following query on the
+production database:
+
+.. sourcecode:: shell
+
+  $ psql -t -c "select version_num from alembic_version;" <database> 
+
+Author, License and Attributions
+--------------------------------
+
+Q3Stats has been created and is developed by
+`Tomek Wójcik <https://www.bthlabs.pl/>`_.
+
+Q3Stats is licensed under the MIT License.
+
+Q3Stats uses the following 3rd party code and resources:
+
+* Bootstrap by Twitter, Inc. and The Bootstrap Authors (MIT),
+* classnames by Jed Watson (MIT),
+* Highcharts,
+* React and ReactDOM by Facebook (BSD),
+* react-bootstrap by Stephen J. Collings, Matthew Honnibal, Pieter Vanderwerff
+  (MIT),
+* redux and react-redux by Dan Abramov (MIT),
+* react-router by Ryan Florence, Michael Jackson (MIT),
+* underscore by Jeremy Ashkenas, DocumentCloud and Investigative
+  Reporters & Editors (MIT).
+
+**NOTE**: Q3Stats uses Highcharts. Please make sure you understand the licesing
+terms of this library and purchase license if needed.
+`Learn more <https://shop.highsoft.com/highcharts/#non-com>`_.

+ 1 - 0
alembic/README

@@ -0,0 +1 @@
+Generic single-database configuration.

+ 72 - 0
alembic/env.py

@@ -0,0 +1,72 @@
+from __future__ import with_statement
+from alembic import context
+from sqlalchemy import engine_from_config, pool
+from logging.config import fileConfig
+
+# this is the Alembic Config object, which provides
+# access to the values within the .ini file in use.
+config = context.config
+
+# Interpret the config file for Python logging.
+# This line sets up loggers basically.
+fileConfig(config.config_file_name)
+
+# add your model's MetaData object here
+# for 'autogenerate' support
+# from myapp import mymodel
+# target_metadata = mymodel.Base.metadata
+target_metadata = None
+
+# other values from the config, defined by the needs of env.py,
+# can be acquired:
+# my_important_option = config.get_main_option("my_important_option")
+# ... etc.
+
+
+def run_migrations_offline():
+    """Run migrations in 'offline' mode.
+
+    This configures the context with just a URL
+    and not an Engine, though an Engine is acceptable
+    here as well.  By skipping the Engine creation
+    we don't even need a DBAPI to be available.
+
+    Calls to context.execute() here emit the given string to the
+    script output.
+
+    """
+    url = config.get_main_option("sqlalchemy.url")
+    context.configure(url=url, target_metadata=target_metadata)
+
+    with context.begin_transaction():
+        context.run_migrations()
+
+
+def run_migrations_online():
+    """Run migrations in 'online' mode.
+
+    In this scenario we need to create an Engine
+    and associate a connection with the context.
+
+    """
+    engine = engine_from_config(
+        config.get_section(config.config_ini_section),
+        prefix='sqlalchemy.',
+        poolclass=pool.NullPool)
+
+    connection = engine.connect()
+    context.configure(
+        connection=connection,
+        target_metadata=target_metadata
+    )
+
+    try:
+        with context.begin_transaction():
+            context.run_migrations()
+    finally:
+        connection.close()
+
+if context.is_offline_mode():
+    run_migrations_offline()
+else:
+    run_migrations_online()

+ 24 - 0
alembic/script.py.mako

@@ -0,0 +1,24 @@
+"""${message}
+
+Revision ID: ${up_revision}
+Revises: ${down_revision | comma,n}
+Create Date: ${create_date}
+
+"""
+
+# revision identifiers, used by Alembic.
+revision = ${repr(up_revision)}
+down_revision = ${repr(down_revision)}
+branch_labels = ${repr(branch_labels)}
+depends_on = ${repr(depends_on)}
+
+from alembic import op
+import sqlalchemy as sa
+${imports if imports else ""}
+
+def upgrade():
+    ${upgrades if upgrades else "pass"}
+
+
+def downgrade():
+    ${downgrades if downgrades else "pass"}

+ 35 - 0
alembic/versions/32ecefbbf8d3_create_table_games.py

@@ -0,0 +1,35 @@
+"""Create table `games`.
+
+Revision ID: 32ecefbbf8d3
+Revises:
+Create Date: 2015-02-21 11:30:09.451628
+
+"""
+
+# revision identifiers, used by Alembic.
+revision = '32ecefbbf8d3'
+down_revision = None
+branch_labels = None
+depends_on = None
+
+from alembic import op
+import sqlalchemy as sa
+import sqlalchemy.dialects.postgresql as pg
+
+
+def upgrade():
+    op.create_table(
+        'games',
+        sa.Column('id', sa.Integer(), primary_key=True),
+        sa.Column('uuid', sa.String(36)),
+        sa.Column('map', sa.String(255), index=True),
+        sa.Column('date', sa.Date(), index=True),
+        sa.Column('time', sa.Time()),
+        sa.Column('fraglimit', sa.Integer()),
+        sa.Column('attrs', pg.JSON()),
+        sa.UniqueConstraint('uuid')
+    )
+
+
+def downgrade():
+    op.drop_table('games')

+ 44 - 0
alembic/versions/3509d93df7c_create_table_scores.py

@@ -0,0 +1,44 @@
+"""Create table `scores`.
+
+Revision ID: 3509d93df7c
+Revises: 32ecefbbf8d3
+Create Date: 2015-02-21 13:27:28.102629
+
+"""
+
+# revision identifiers, used by Alembic.
+revision = '3509d93df7c'
+down_revision = '32ecefbbf8d3'
+branch_labels = None
+depends_on = None
+
+from alembic import op
+import sqlalchemy as sa
+import sqlalchemy.dialects.postgresql as pg
+
+
+def upgrade():
+    op.create_table(
+        'scores',
+        sa.Column('id', sa.Integer(), primary_key=True),
+        sa.Column(
+            'game_id', sa.Integer(), sa.ForeignKey('games.id'), index=True
+        ),
+        sa.Column('player', sa.String(255), index=True),
+        sa.Column('score', sa.Integer()),
+        sa.Column('kills', sa.Integer()),
+        sa.Column('deaths', sa.Integer()),
+        sa.Column('suicides', sa.Integer()),
+        sa.Column('net', sa.Integer()),
+        sa.Column('damage_taken', sa.Integer()),
+        sa.Column('damage_given', sa.Integer()),
+        sa.Column('total_health', sa.Integer()),
+        sa.Column('total_armor', sa.Integer()),
+        sa.Column('weapons', pg.JSON()),
+        sa.Column('items', pg.JSON()),
+        sa.Column('powerups', pg.JSON()),
+    )
+
+
+def downgrade():
+    op.drop_table('scores')

+ 53 - 0
frontend/.eslintrc.json

@@ -0,0 +1,53 @@
+{
+  "env": {
+    "browser": true,
+    "commonjs": true,
+    "es6": true
+  },
+  "extends": "eslint:recommended",
+  "parserOptions": {
+    "sourceType":"module",
+    "ecmaFeatures": {
+      "jsx": true,
+      "modules": true
+    }
+  },
+  "rules": {
+    "no-undef": "error",
+    "quotes": [
+        2, "double", {"avoidEscape": true, "allowTemplateLiterals": true}
+    ],
+    "no-unused-vars": [0],
+    "no-console": [2],
+    "no-empty": ["error", {"allowEmptyCatch": true}],
+    "array-bracket-spacing": ["error", "never"],
+    "block-spacing": ["error", "always"],
+    "brace-style": ["error", "1tbs", {"allowSingleLine": true}],
+    "camelcase": ["error", {"properties": "never"}],
+    "comma-dangle": ["error", "never"],
+    "comma-spacing": ["error", {"before": false, "after": true}],
+    "comma-style": ["error", "last"],
+    "computed-property-spacing": ["error", "never"],
+    "key-spacing": [
+        "error", {"beforeColon": false, "afterColon": true, "mode": "strict"}
+    ],
+    "keyword-spacing": ["error", { "before": true, "after": true }],
+    "linebreak-style": ["error", "unix"],
+    "max-len": ["error", 120],
+    "no-multiple-empty-lines": ["error"],
+    "no-spaced-func": ["error"],
+    "no-trailing-spaces": ["error"],
+    "no-unreachable": [1],
+    "no-whitespace-before-property": ["error"],
+    "object-curly-spacing": ["error", "never"],
+    "one-var-declaration-per-line": ["error", "always"],
+    "one-var": ["error", "never"],
+    "semi-spacing": ["error", {"before": false, "after": true}],
+    "semi": ["error", "always"],
+    "space-before-function-paren": ["error", "always"],
+    "space-before-blocks": ["error", "always"],
+    "space-in-parens": ["error", "never"],
+    "space-infix-ops": ["error"],
+    "unicode-bom": ["error", "never"]
+  }
+}

+ 7 - 0
frontend/.gitignore

@@ -0,0 +1,7 @@
+.build/
+.sass-cache/
+node_modules/
+
+img/items/*.png
+img/powerups/*.png
+img/weapons/*.png

+ 143 - 0
frontend/Gruntfile.js

@@ -0,0 +1,143 @@
+var path = require("path");
+var child_process = require("child_process");
+
+var underscore = require("underscore");
+var webpack = require("webpack");
+
+var BUILD_DIR = path.resolve(__dirname, "..", "q3stats", "web_app");
+var STATIC_DIR = path.resolve(BUILD_DIR, "static");
+var HTML_DIR = path.resolve(BUILD_DIR, "templates");
+var VERSION = child_process.execSync("git rev-parse HEAD").toString("utf-8").trim();
+
+var devWebpackConfig = require("./webpack.config.js");
+var distWebpackConfig = underscore.extend(
+  underscore.clone(devWebpackConfig),
+  {
+    devtool: false,
+    output: {
+      path: path.resolve(STATIC_DIR, "js"),
+      filename: "q3stats-" + VERSION + ".min.js"
+    },
+    resolve: {
+      alias: {
+        "backbone": "backbone/backbone-min.js",
+        "jquery": "jquery/dist/jquery.min.js",
+        "react": "react/dist/react.min.js",
+        "react-dom": "react-dom/dist/react-dom.min.js",
+        "redux": "redux/dist/redux.min.js",
+        "underscore": "underscore/underscore-min.js"
+      }
+    },
+    plugins: [
+      new webpack.optimize.UglifyJsPlugin({minimize: true})
+    ]
+  }
+);
+
+module.exports = function (grunt) {
+  grunt.loadNpmTasks("grunt-contrib-clean");
+  grunt.loadNpmTasks("grunt-contrib-copy");
+  grunt.loadNpmTasks("grunt-contrib-sass");
+  grunt.loadNpmTasks("grunt-contrib-watch");
+  grunt.loadNpmTasks("grunt-webpack");
+  grunt.loadNpmTasks("gruntify-eslint");
+
+  grunt.initConfig({
+    clean: {
+      options: {
+        force: true,
+      },
+      all: [STATIC_DIR]
+    },
+    copy: {
+      assets: {
+        files: [
+          {
+            cwd: "",
+            dot: true,
+            dest: STATIC_DIR,
+            expand: true,
+            src: ["./fonts/**/*", "./img/**/*.png"]
+          }
+        ]
+      },
+      devHtml: {
+        files: [
+          {
+            cwd: "",
+            dot: true,
+            dest: HTML_DIR,
+            expand: true,
+            src: ["./index.html", "./error.html"]
+          }
+        ]
+      },
+      distHtml: {
+        cwd: "",
+        dot: true,
+        dest: HTML_DIR,
+        expand: true,
+        src: ["./index.html", "./error.html"],
+        options: {
+          process: function (content, srcPath) {
+            return content.replace(
+              /q3stats\.(css|js)/g, "q3stats-" + VERSION + ".min.$1"
+            );
+          }
+        }
+      }
+    },
+    eslint: {
+      sources: {
+        src: ["./src/**/*.js"]
+      }
+    },
+    sass: {
+      dev: {
+        src: ["./sass/main.scss"],
+        dest: path.resolve(STATIC_DIR, "css", "q3stats.css"),
+        options: {
+          compass: true,
+          precision: 10,
+          sourcemap: "auto",
+          style: "expanded"
+        }
+      },
+      dist: {
+        src: ["./sass/main.scss"],
+        dest: path.resolve(
+          STATIC_DIR, "css", "q3stats-" + VERSION + ".min.css"
+        ),
+        options: {
+          compass: true,
+          precision: 10,
+          sourcemap: "none",
+          style: "compressed"
+        }
+      }
+    },
+    watch: {
+      sass: {
+        files: ["./sass/**/*.scss", "./img/**/*.png"],
+        tasks: ["sass:dev", "copy:assets"]
+      },
+      js: {
+        files: ["./src/**/*.js", "./index.html", "./error.html"],
+        tasks: ["webpack:dev", "copy:devHtml"]
+      }
+    },
+    webpack: {
+      dev: devWebpackConfig,
+      dist: distWebpackConfig
+    }
+  });
+
+  grunt.registerTask("dev", [
+    "clean", "webpack:dev", "sass:dev", "copy:assets", "copy:devHtml"
+  ]);
+
+  grunt.registerTask("dist", [
+    "clean", "eslint", "webpack:dist", "sass:dist", "copy:assets",
+    "copy:distHtml"
+  ]);
+};

+ 37 - 0
frontend/error.html

@@ -0,0 +1,37 @@
+<!DOCTYPE html>
+<!--
+                     A
+ _     _   _     _       _                 _ 
+| |__ | |_| |__ | | __ _| |__  ___   _ __ | |
+| '_ \| __| '_ \| |/ _` | '_ \/ __| | '_ \| |
+| |_) | |_| | | | | (_| | |_) \__ \_| |_) | |
+|_.__/ \__|_| |_|_|\__,_|_.__/|___(_) .__/|_|
+                                    |_|      
+                production
+-->
+<html lang="en">
+  <head>
+    <meta charset="utf-8">
+    <meta http-equiv="X-UA-Compatible" content="IE=edge">
+    <meta name="viewport" content="width=device-width, initial-scale=1">
+    <title>Q3Stats</title>
+
+    <link href="static/css/q3stats.css" rel="stylesheet">
+    <link rel="shortcut icon" href="static/img/powerups/Quad.png">
+  </head>
+
+  <body id="server-error" class="error-{{ error_code }}">
+    <div class="q3stats-error-view">
+      <div class="container">
+        <div class="row">
+          <div class="col-sm-6 col-sm-offset-3 text-center">
+            <h2>Whoops!</h2>
+            <p class="lead">Apparently something went wrong. Sorry for that.</p>
+            <p><a class="btn btn-primary" href="{{ url_for('frontend.get_frontend_index') }}">Go to the app &raquo;</a></p>
+            <p class="text-muted"><small>Error code: {{ error_code }}</small></p>
+          </div>
+        </div>
+      </div>
+    </div>
+  </body>
+</html>

BIN
frontend/fonts/glyphicons-halflings-regular.eot


File diff suppressed because it is too large
+ 288 - 0
frontend/fonts/glyphicons-halflings-regular.svg


BIN
frontend/fonts/glyphicons-halflings-regular.ttf


BIN
frontend/fonts/glyphicons-halflings-regular.woff


BIN
frontend/fonts/glyphicons-halflings-regular.woff2


+ 0 - 0
frontend/img/items/.placeholder


+ 0 - 0
frontend/img/powerups/.placeholder


+ 0 - 0
frontend/img/weapons/.placeholder


+ 34 - 0
frontend/index.html

@@ -0,0 +1,34 @@
+<!DOCTYPE html>
+<!--
+                     A
+ _     _   _     _       _                 _ 
+| |__ | |_| |__ | | __ _| |__  ___   _ __ | |
+| '_ \| __| '_ \| |/ _` | '_ \/ __| | '_ \| |
+| |_) | |_| | | | | (_| | |_) \__ \_| |_) | |
+|_.__/ \__|_| |_|_|\__,_|_.__/|___(_) .__/|_|
+                                    |_|      
+                production
+-->
+<html lang="en">
+  <head>
+    <meta charset="utf-8">
+    <meta http-equiv="X-UA-Compatible" content="IE=edge">
+    <meta name="viewport" content="width=device-width, initial-scale=1">
+    <title>Q3Stats</title>
+    <meta name="generator" content="Q3Stats v{{ version }} by Tomek Wójcik">
+
+    <link href="static/css/q3stats.css" rel="stylesheet">
+    <link rel="shortcut icon" href="static/img/powerups/Quad.png">
+
+    <script type="text/javascript">
+      var Q3STATS = {
+        version: "{{ version }}"
+      };
+    </script>
+  </head>
+
+  <body>
+    <div id="main"></div>
+    <script type="text/javascript" src="static/js/q3stats.js"></script>
+  </body>
+</html>

+ 38 - 0
frontend/package.json

@@ -0,0 +1,38 @@
+{
+  "name": "q3stats-frontend",
+  "version": "1.0.0",
+  "description": "Q3Stats Frontend",
+  "main": "index.js",
+  "author": "Tomek Wójcik",
+  "license": "MIT",
+  "repository": "https://git.bthlabs.pl/tomekwojcik/q3stats",
+  "dependencies": {
+    "classnames": "^2.2.5",
+    "highcharts": "^5.0.7",
+    "react": "^15.4.2",
+    "react-bootstrap": "^0.30.7",
+    "react-dom": "^15.4.2",
+    "react-redux": "^5.0.3",
+    "react-router": "^3.0.2",
+    "redux": "^3.6.0",
+    "underscore": "^1.8.3",
+    "whatwg-fetch": "^2.0.2"
+  },
+  "devDependencies": {
+    "babel-core": "^6.22.1",
+    "babel-loader": "^6.2.10",
+    "babel-preset-es2015": "^6.22.0",
+    "babel-preset-react": "^6.22.0",
+    "grunt": "^1.0.1",
+    "grunt-contrib-clean": "^1.0.0",
+    "grunt-contrib-copy": "^1.0.0",
+    "grunt-contrib-sass": "^1.0.0",
+    "grunt-contrib-watch": "^1.0.0",
+    "grunt-webpack": "^2.0.1",
+    "gruntify-eslint": "^3.1.0",
+    "imports-loader": "^0.7.0",
+    "webpack": "^2.2.1",
+    "webpack-dev-server": "^2.2.1",
+    "webpack-uglify-js-plugin": "^1.1.9"
+  }
+}

+ 36 - 0
frontend/sass/_mixins.scss

@@ -0,0 +1,36 @@
+@mixin loading {
+  -webkit-animation: anim-loader-inner 1s infinite linear;
+  animation: anim-loader-inner 1s infinite linear;
+  background: transparent url('../img/powerups/Quad.png') no-repeat;
+  height: 64px;
+  left: 50%;
+  margin-left: -32px;
+  margin-top: -32px;
+  position: absolute;
+  -webkit-transform: translateZ(0);
+  -ms-transform: translateZ(0);
+  transform: translateZ(0);
+  top: 50%;
+  width: 64px;
+
+  @-webkit-keyframes anim-loader-inner {
+    0% {
+      -webkit-transform: rotate(0deg);
+      transform: rotate(0deg);
+    }
+    100% {
+      -webkit-transform: rotate(360deg);
+      transform: rotate(360deg);
+    }
+  }
+  @keyframes anim-loader-inner {
+    0% {
+      -webkit-transform: rotate(0deg);
+      transform: rotate(0deg);
+    }
+    100% {
+      -webkit-transform: rotate(360deg);
+      transform: rotate(360deg);
+    }
+  }
+}

+ 20 - 0
frontend/sass/components/ChartComponent.scss

@@ -0,0 +1,20 @@
+.q3stats-chart {
+  height: 400px;
+
+  .highcharts-loading {
+    opacity: 1 !important;
+
+    .highcharts-loading-inner {
+      position: static !important;
+      top: 0px !important;
+      visibility: hidden;
+
+      &::after {
+        @include loading;
+        content: '';
+        display: block;
+        visibility: visible;
+      }
+    }
+  }
+}

+ 21 - 0
frontend/sass/components/LoaderComponent.scss

@@ -0,0 +1,21 @@
+.q3stats-loader {
+  background-color: white;
+  bottom: 0px;
+  display: block;
+  left: 0px;
+  opacity: 0;
+  pointer-events: none;
+  position: absolute;
+  right: 0px;
+  top: 0px;
+  transition: opacity 0.3s;
+
+  &.visible {
+    opacity: 1;
+    pointer-events: auto;
+  }
+
+  .inner {
+    @include loading;
+  }
+}

+ 43 - 0
frontend/sass/main.scss

@@ -0,0 +1,43 @@
+@import "vendor/_bootstrap";
+
+/*!
+ * Q3Stats CSS | Copyright 2017 by Tomek Wójcik | MIT License
+ * https://git.bthlabs.pl/q3stats
+ */
+
+@import "_mixins";
+
+@import "components/ChartComponent.scss";
+@import "components/LoaderComponent.scss";
+
+@import "views/AboutModalView.scss";
+@import "views/ErrorView.scss";
+@import "views/NavigationBarView.scss";
+@import "views/PlayerGameView.scss";
+
+table td > img {
+  border-radius: 3px;
+  background: #eeeeee;
+  height: 24px;
+}
+
+.q3stats-app-window {
+  align-content: stretch;
+  display: flex;
+  flex-direction: column;
+  height: 100vh;
+}
+
+.q3stats-content-view {
+  flex: 2 100%;
+  order: 2;
+  overflow-y: scroll;
+  position: relative;
+}
+
+.q3stats-footer {
+  background-color: $navbar-default-bg;
+  border-top: 1px solid $navbar-default-border;
+  order: 3;
+  padding-top: ($line-height-computed / 2);
+}

+ 56 - 0
frontend/sass/vendor/_bootstrap.scss

@@ -0,0 +1,56 @@
+/*!
+ * Bootstrap v3.3.7 (http://getbootstrap.com)
+ * Copyright 2011-2016 Twitter, Inc.
+ * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)
+ */
+
+// Core variables and mixins
+@import "bootstrap/variables";
+@import "bootstrap/mixins";
+
+// Reset and dependencies
+@import "bootstrap/normalize";
+@import "bootstrap/print";
+@import "bootstrap/glyphicons";
+
+// Core CSS
+@import "bootstrap/scaffolding";
+@import "bootstrap/type";
+@import "bootstrap/code";
+@import "bootstrap/grid";
+@import "bootstrap/tables";
+@import "bootstrap/forms";
+@import "bootstrap/buttons";
+
+// Components
+@import "bootstrap/component-animations";
+@import "bootstrap/dropdowns";
+@import "bootstrap/button-groups";
+@import "bootstrap/input-groups";
+@import "bootstrap/navs";
+@import "bootstrap/navbar";
+@import "bootstrap/breadcrumbs";
+@import "bootstrap/pagination";
+@import "bootstrap/pager";
+@import "bootstrap/labels";
+@import "bootstrap/badges";
+@import "bootstrap/jumbotron";
+@import "bootstrap/thumbnails";
+@import "bootstrap/alerts";
+@import "bootstrap/progress-bars";
+@import "bootstrap/media";
+@import "bootstrap/list-group";
+@import "bootstrap/panels";
+@import "bootstrap/responsive-embed";
+@import "bootstrap/wells";
+@import "bootstrap/close";
+
+// Components w/ JavaScript
+@import "bootstrap/modals";
+@import "bootstrap/tooltip";
+@import "bootstrap/popovers";
+@import "bootstrap/carousel";
+
+// Utility classes
+@import "bootstrap/utilities";
+@import "bootstrap/responsive-utilities";

+ 73 - 0
frontend/sass/vendor/bootstrap/_alerts.scss

@@ -0,0 +1,73 @@
+//
+// Alerts
+// --------------------------------------------------
+
+
+// Base styles
+// -------------------------
+
+.alert {
+  padding: $alert-padding;
+  margin-bottom: $line-height-computed;
+  border: 1px solid transparent;
+  border-radius: $alert-border-radius;
+
+  // Headings for larger alerts
+  h4 {
+    margin-top: 0;
+    // Specified for the h4 to prevent conflicts of changing $headings-color
+    color: inherit;
+  }
+
+  // Provide class for links that match alerts
+  .alert-link {
+    font-weight: $alert-link-font-weight;
+  }
+
+  // Improve alignment and spacing of inner content
+  > p,
+  > ul {
+    margin-bottom: 0;
+  }
+
+  > p + p {
+    margin-top: 5px;
+  }
+}
+
+// Dismissible alerts
+//
+// Expand the right padding and account for the close button's positioning.
+
+.alert-dismissable, // The misspelled .alert-dismissable was deprecated in 3.2.0.
+.alert-dismissible {
+  padding-right: ($alert-padding + 20);
+
+  // Adjust close link position
+  .close {
+    position: relative;
+    top: -2px;
+    right: -21px;
+    color: inherit;
+  }
+}
+
+// Alternate styles
+//
+// Generate contextual modifier classes for colorizing the alert.
+
+.alert-success {
+  @include alert-variant($alert-success-bg, $alert-success-border, $alert-success-text);
+}
+
+.alert-info {
+  @include alert-variant($alert-info-bg, $alert-info-border, $alert-info-text);
+}
+
+.alert-warning {
+  @include alert-variant($alert-warning-bg, $alert-warning-border, $alert-warning-text);
+}
+
+.alert-danger {
+  @include alert-variant($alert-danger-bg, $alert-danger-border, $alert-danger-text);
+}

+ 68 - 0
frontend/sass/vendor/bootstrap/_badges.scss

@@ -0,0 +1,68 @@
+//
+// Badges
+// --------------------------------------------------
+
+
+// Base class
+.badge {
+  display: inline-block;
+  min-width: 10px;
+  padding: 3px 7px;
+  font-size: $font-size-small;
+  font-weight: $badge-font-weight;
+  color: $badge-color;
+  line-height: $badge-line-height;
+  vertical-align: middle;
+  white-space: nowrap;
+  text-align: center;
+  background-color: $badge-bg;
+  border-radius: $badge-border-radius;
+
+  // Empty badges collapse automatically (not available in IE8)
+  &:empty {
+    display: none;
+  }
+
+  // Quick fix for badges in buttons
+  .btn & {
+    position: relative;
+    top: -1px;
+  }
+
+  .btn-xs &,
+  .btn-group-xs > .btn & {
+    top: 0;
+    padding: 1px 5px;
+  }
+
+  // [converter] extracted a& to a.badge
+
+  // Account for badges in navs
+  .list-group-item.active > &,
+  .nav-pills > .active > a > & {
+    color: $badge-active-color;
+    background-color: $badge-active-bg;
+  }
+
+  .list-group-item > & {
+    float: right;
+  }
+
+  .list-group-item > & + & {
+    margin-right: 5px;
+  }
+
+  .nav-pills > li > a > & {
+    margin-left: 3px;
+  }
+}
+
+// Hover state, but only for links
+a.badge {
+  &:hover,
+  &:focus {
+    color: $badge-link-hover-color;
+    text-decoration: none;
+    cursor: pointer;
+  }
+}

+ 28 - 0
frontend/sass/vendor/bootstrap/_breadcrumbs.scss

@@ -0,0 +1,28 @@
+//
+// Breadcrumbs
+// --------------------------------------------------
+
+
+.breadcrumb {
+  padding: $breadcrumb-padding-vertical $breadcrumb-padding-horizontal;
+  margin-bottom: $line-height-computed;
+  list-style: none;
+  background-color: $breadcrumb-bg;
+  border-radius: $border-radius-base;
+
+  > li {
+    display: inline-block;
+
+    + li:before {
+      // [converter] Workaround for https://github.com/sass/libsass/issues/1115
+      $nbsp: "\00a0";
+      content: "#{$breadcrumb-separator}#{$nbsp}"; // Unicode space added since inline-block means non-collapsing white-space
+      padding: 0 5px;
+      color: $breadcrumb-color;
+    }
+  }
+
+  > .active {
+    color: $breadcrumb-active-color;
+  }
+}

+ 244 - 0
frontend/sass/vendor/bootstrap/_button-groups.scss

@@ -0,0 +1,244 @@
+//
+// Button groups
+// --------------------------------------------------
+
+// Make the div behave like a button
+.btn-group,
+.btn-group-vertical {
+  position: relative;
+  display: inline-block;
+  vertical-align: middle; // match .btn alignment given font-size hack above
+  > .btn {
+    position: relative;
+    float: left;
+    // Bring the "active" button to the front
+    &:hover,
+    &:focus,
+    &:active,
+    &.active {
+      z-index: 2;
+    }
+  }
+}
+
+// Prevent double borders when buttons are next to each other
+.btn-group {
+  .btn + .btn,
+  .btn + .btn-group,
+  .btn-group + .btn,
+  .btn-group + .btn-group {
+    margin-left: -1px;
+  }
+}
+
+// Optional: Group multiple button groups together for a toolbar
+.btn-toolbar {
+  margin-left: -5px; // Offset the first child's margin
+  @include clearfix;
+
+  .btn,
+  .btn-group,
+  .input-group {
+    float: left;
+  }
+  > .btn,
+  > .btn-group,
+  > .input-group {
+    margin-left: 5px;
+  }
+}
+
+.btn-group > .btn:not(:first-child):not(:last-child):not(.dropdown-toggle) {
+  border-radius: 0;
+}
+
+// Set corners individual because sometimes a single button can be in a .btn-group and we need :first-child and :last-child to both match
+.btn-group > .btn:first-child {
+  margin-left: 0;
+  &:not(:last-child):not(.dropdown-toggle) {
+    @include border-right-radius(0);
+  }
+}
+// Need .dropdown-toggle since :last-child doesn't apply, given that a .dropdown-menu is used immediately after it
+.btn-group > .btn:last-child:not(:first-child),
+.btn-group > .dropdown-toggle:not(:first-child) {
+  @include border-left-radius(0);
+}
+
+// Custom edits for including btn-groups within btn-groups (useful for including dropdown buttons within a btn-group)
+.btn-group > .btn-group {
+  float: left;
+}
+.btn-group > .btn-group:not(:first-child):not(:last-child) > .btn {
+  border-radius: 0;
+}
+.btn-group > .btn-group:first-child:not(:last-child) {
+  > .btn:last-child,
+  > .dropdown-toggle {
+    @include border-right-radius(0);
+  }
+}
+.btn-group > .btn-group:last-child:not(:first-child) > .btn:first-child {
+  @include border-left-radius(0);
+}
+
+// On active and open, don't show outline
+.btn-group .dropdown-toggle:active,
+.btn-group.open .dropdown-toggle {
+  outline: 0;
+}
+
+
+// Sizing
+//
+// Remix the default button sizing classes into new ones for easier manipulation.
+
+.btn-group-xs > .btn { @extend .btn-xs; }
+.btn-group-sm > .btn { @extend .btn-sm; }
+.btn-group-lg > .btn { @extend .btn-lg; }
+
+
+// Split button dropdowns
+// ----------------------
+
+// Give the line between buttons some depth
+.btn-group > .btn + .dropdown-toggle {
+  padding-left: 8px;
+  padding-right: 8px;
+}
+.btn-group > .btn-lg + .dropdown-toggle {
+  padding-left: 12px;
+  padding-right: 12px;
+}
+
+// The clickable button for toggling the menu
+// Remove the gradient and set the same inset shadow as the :active state
+.btn-group.open .dropdown-toggle {
+  @include box-shadow(inset 0 3px 5px rgba(0,0,0,.125));
+
+  // Show no shadow for `.btn-link` since it has no other button styles.
+  &.btn-link {
+    @include box-shadow(none);
+  }
+}
+
+
+// Reposition the caret
+.btn .caret {
+  margin-left: 0;
+}
+// Carets in other button sizes
+.btn-lg .caret {
+  border-width: $caret-width-large $caret-width-large 0;
+  border-bottom-width: 0;
+}
+// Upside down carets for .dropup
+.dropup .btn-lg .caret {
+  border-width: 0 $caret-width-large $caret-width-large;
+}
+
+
+// Vertical button groups
+// ----------------------
+
+.btn-group-vertical {
+  > .btn,
+  > .btn-group,
+  > .btn-group > .btn {
+    display: block;
+    float: none;
+    width: 100%;
+    max-width: 100%;
+  }
+
+  // Clear floats so dropdown menus can be properly placed
+  > .btn-group {
+    @include clearfix;
+    > .btn {
+      float: none;
+    }
+  }
+
+  > .btn + .btn,
+  > .btn + .btn-group,
+  > .btn-group + .btn,
+  > .btn-group + .btn-group {
+    margin-top: -1px;
+    margin-left: 0;
+  }
+}
+
+.btn-group-vertical > .btn {
+  &:not(:first-child):not(:last-child) {
+    border-radius: 0;
+  }
+  &:first-child:not(:last-child) {
+    @include border-top-radius($btn-border-radius-base);
+    @include border-bottom-radius(0);
+  }
+  &:last-child:not(:first-child) {
+    @include border-top-radius(0);
+    @include border-bottom-radius($btn-border-radius-base);
+  }
+}
+.btn-group-vertical > .btn-group:not(:first-child):not(:last-child) > .btn {
+  border-radius: 0;
+}
+.btn-group-vertical > .btn-group:first-child:not(:last-child) {
+  > .btn:last-child,
+  > .dropdown-toggle {
+    @include border-bottom-radius(0);
+  }
+}
+.btn-group-vertical > .btn-group:last-child:not(:first-child) > .btn:first-child {
+  @include border-top-radius(0);
+}
+
+
+// Justified button groups
+// ----------------------
+
+.btn-group-justified {
+  display: table;
+  width: 100%;
+  table-layout: fixed;
+  border-collapse: separate;
+  > .btn,
+  > .btn-group {
+    float: none;
+    display: table-cell;
+    width: 1%;
+  }
+  > .btn-group .btn {
+    width: 100%;
+  }
+
+  > .btn-group .dropdown-menu {
+    left: auto;
+  }
+}
+
+
+// Checkbox and radio options
+//
+// In order to support the browser's form validation feedback, powered by the
+// `required` attribute, we have to "hide" the inputs via `clip`. We cannot use
+// `display: none;` or `visibility: hidden;` as that also hides the popover.
+// Simply visually hiding the inputs via `opacity` would leave them clickable in
+// certain cases which is prevented by using `clip` and `pointer-events`.
+// This way, we ensure a DOM element is visible to position the popover from.
+//
+// See https://github.com/twbs/bootstrap/pull/12794 and
+// https://github.com/twbs/bootstrap/pull/14559 for more information.
+
+[data-toggle="buttons"] {
+  > .btn,
+  > .btn-group > .btn {
+    input[type="radio"],
+    input[type="checkbox"] {
+      position: absolute;
+      clip: rect(0,0,0,0);
+      pointer-events: none;
+    }
+  }
+}

+ 168 - 0
frontend/sass/vendor/bootstrap/_buttons.scss

@@ -0,0 +1,168 @@
+//
+// Buttons
+// --------------------------------------------------
+
+
+// Base styles
+// --------------------------------------------------
+
+.btn {
+  display: inline-block;
+  margin-bottom: 0; // For input.btn
+  font-weight: $btn-font-weight;
+  text-align: center;
+  vertical-align: middle;
+  touch-action: manipulation;
+  cursor: pointer;
+  background-image: none; // Reset unusual Firefox-on-Android default style; see https://github.com/necolas/normalize.css/issues/214
+  border: 1px solid transparent;
+  white-space: nowrap;
+  @include button-size($padding-base-vertical, $padding-base-horizontal, $font-size-base, $line-height-base, $btn-border-radius-base);
+  @include user-select(none);
+
+  &,
+  &:active,
+  &.active {
+    &:focus,
+    &.focus {
+      @include tab-focus;
+    }
+  }
+
+  &:hover,
+  &:focus,
+  &.focus {
+    color: $btn-default-color;
+    text-decoration: none;
+  }
+
+  &:active,
+  &.active {
+    outline: 0;
+    background-image: none;
+    @include box-shadow(inset 0 3px 5px rgba(0,0,0,.125));
+  }
+
+  &.disabled,
+  &[disabled],
+  fieldset[disabled] & {
+    cursor: $cursor-disabled;
+    @include opacity(.65);
+    @include box-shadow(none);
+  }
+
+  // [converter] extracted a& to a.btn
+}
+
+a.btn {
+  &.disabled,
+  fieldset[disabled] & {
+    pointer-events: none; // Future-proof disabling of clicks on `<a>` elements
+  }
+}
+
+
+// Alternate buttons
+// --------------------------------------------------
+
+.btn-default {
+  @include button-variant($btn-default-color, $btn-default-bg, $btn-default-border);
+}
+.btn-primary {
+  @include button-variant($btn-primary-color, $btn-primary-bg, $btn-primary-border);
+}
+// Success appears as green
+.btn-success {
+  @include button-variant($btn-success-color, $btn-success-bg, $btn-success-border);
+}
+// Info appears as blue-green
+.btn-info {
+  @include button-variant($btn-info-color, $btn-info-bg, $btn-info-border);
+}
+// Warning appears as orange
+.btn-warning {
+  @include button-variant($btn-warning-color, $btn-warning-bg, $btn-warning-border);
+}
+// Danger and error appear as red
+.btn-danger {
+  @include button-variant($btn-danger-color, $btn-danger-bg, $btn-danger-border);
+}
+
+
+// Link buttons
+// -------------------------
+
+// Make a button look and behave like a link
+.btn-link {
+  color: $link-color;
+  font-weight: normal;
+  border-radius: 0;
+
+  &,
+  &:active,
+  &.active,
+  &[disabled],
+  fieldset[disabled] & {
+    background-color: transparent;
+    @include box-shadow(none);
+  }
+  &,
+  &:hover,
+  &:focus,
+  &:active {
+    border-color: transparent;
+  }
+  &:hover,
+  &:focus {
+    color: $link-hover-color;
+    text-decoration: $link-hover-decoration;
+    background-color: transparent;
+  }
+  &[disabled],
+  fieldset[disabled] & {
+    &:hover,
+    &:focus {
+      color: $btn-link-disabled-color;
+      text-decoration: none;
+    }
+  }
+}
+
+
+// Button Sizes
+// --------------------------------------------------
+
+.btn-lg {
+  // line-height: ensure even-numbered height of button next to large input
+  @include button-size($padding-large-vertical, $padding-large-horizontal, $font-size-large, $line-height-large, $btn-border-radius-large);
+}
+.btn-sm {
+  // line-height: ensure proper height of button next to small input
+  @include button-size($padding-small-vertical, $padding-small-horizontal, $font-size-small, $line-height-small, $btn-border-radius-small);
+}
+.btn-xs {
+  @include button-size($padding-xs-vertical, $padding-xs-horizontal, $font-size-small, $line-height-small, $btn-border-radius-small);
+}
+
+
+// Block button
+// --------------------------------------------------
+
+.btn-block {
+  display: block;
+  width: 100%;
+}
+
+// Vertically space out multiple block buttons
+.btn-block + .btn-block {
+  margin-top: 5px;
+}
+
+// Specificity overrides
+input[type="submit"],
+input[type="reset"],
+input[type="button"] {
+  &.btn-block {
+    width: 100%;
+  }
+}

+ 270 - 0
frontend/sass/vendor/bootstrap/_carousel.scss

@@ -0,0 +1,270 @@
+//
+// Carousel
+// --------------------------------------------------
+
+
+// Wrapper for the slide container and indicators
+.carousel {
+  position: relative;
+}
+
+.carousel-inner {
+  position: relative;
+  overflow: hidden;
+  width: 100%;
+
+  > .item {
+    display: none;
+    position: relative;
+    @include transition(.6s ease-in-out left);
+
+    // Account for jankitude on images
+    > img,
+    > a > img {
+      @include img-responsive;
+      line-height: 1;
+    }
+
+    // WebKit CSS3 transforms for supported devices
+    @media all and (transform-3d), (-webkit-transform-3d) {
+      @include transition-transform(0.6s ease-in-out);
+      @include backface-visibility(hidden);
+      @include perspective(1000px);
+
+      &.next,
+      &.active.right {
+        @include translate3d(100%, 0, 0);
+        left: 0;
+      }
+      &.prev,
+      &.active.left {
+        @include translate3d(-100%, 0, 0);
+        left: 0;
+      }
+      &.next.left,
+      &.prev.right,
+      &.active {
+        @include translate3d(0, 0, 0);
+        left: 0;
+      }
+    }
+  }
+
+  > .active,
+  > .next,
+  > .prev {
+    display: block;
+  }
+
+  > .active {
+    left: 0;
+  }
+
+  > .next,
+  > .prev {
+    position: absolute;
+    top: 0;
+    width: 100%;
+  }
+
+  > .next {
+    left: 100%;
+  }
+  > .prev {
+    left: -100%;
+  }
+  > .next.left,
+  > .prev.right {
+    left: 0;
+  }
+
+  > .active.left {
+    left: -100%;
+  }
+  > .active.right {
+    left: 100%;
+  }
+
+}
+
+// Left/right controls for nav
+// ---------------------------
+
+.carousel-control {
+  position: absolute;
+  top: 0;
+  left: 0;
+  bottom: 0;
+  width: $carousel-control-width;
+  @include opacity($carousel-control-opacity);
+  font-size: $carousel-control-font-size;
+  color: $carousel-control-color;
+  text-align: center;
+  text-shadow: $carousel-text-shadow;
+  background-color: rgba(0, 0, 0, 0); // Fix IE9 click-thru bug
+  // We can't have this transition here because WebKit cancels the carousel
+  // animation if you trip this while in the middle of another animation.
+
+  // Set gradients for backgrounds
+  &.left {
+    @include gradient-horizontal($start-color: rgba(0,0,0,.5), $end-color: rgba(0,0,0,.0001));
+  }
+  &.right {
+    left: auto;
+    right: 0;
+    @include gradient-horizontal($start-color: rgba(0,0,0,.0001), $end-color: rgba(0,0,0,.5));
+  }
+
+  // Hover/focus state
+  &:hover,
+  &:focus {
+    outline: 0;
+    color: $carousel-control-color;
+    text-decoration: none;
+    @include opacity(.9);
+  }
+
+  // Toggles
+  .icon-prev,
+  .icon-next,
+  .glyphicon-chevron-left,
+  .glyphicon-chevron-right {
+    position: absolute;
+    top: 50%;
+    margin-top: -10px;
+    z-index: 5;
+    display: inline-block;
+  }
+  .icon-prev,
+  .glyphicon-chevron-left {
+    left: 50%;
+    margin-left: -10px;
+  }
+  .icon-next,
+  .glyphicon-chevron-right {
+    right: 50%;
+    margin-right: -10px;
+  }
+  .icon-prev,
+  .icon-next {
+    width:  20px;
+    height: 20px;
+    line-height: 1;
+    font-family: serif;
+  }
+
+
+  .icon-prev {
+    &:before {
+      content: '\2039';// SINGLE LEFT-POINTING ANGLE QUOTATION MARK (U+2039)
+    }
+  }
+  .icon-next {
+    &:before {
+      content: '\203a';// SINGLE RIGHT-POINTING ANGLE QUOTATION MARK (U+203A)
+    }
+  }
+}
+
+// Optional indicator pips
+//
+// Add an unordered list with the following class and add a list item for each
+// slide your carousel holds.
+
+.carousel-indicators {
+  position: absolute;
+  bottom: 10px;
+  left: 50%;
+  z-index: 15;
+  width: 60%;
+  margin-left: -30%;
+  padding-left: 0;
+  list-style: none;
+  text-align: center;
+
+  li {
+    display: inline-block;
+    width:  10px;
+    height: 10px;
+    margin: 1px;
+    text-indent: -999px;
+    border: 1px solid $carousel-indicator-border-color;
+    border-radius: 10px;
+    cursor: pointer;
+
+    // IE8-9 hack for event handling
+    //
+    // Internet Explorer 8-9 does not support clicks on elements without a set
+    // `background-color`. We cannot use `filter` since that's not viewed as a
+    // background color by the browser. Thus, a hack is needed.
+    // See https://developer.mozilla.org/en-US/docs/Web/Events/click#Internet_Explorer
+    //
+    // For IE8, we set solid black as it doesn't support `rgba()`. For IE9, we
+    // set alpha transparency for the best results possible.
+    background-color: #000 \9; // IE8
+    background-color: rgba(0,0,0,0); // IE9
+  }
+  .active {
+    margin: 0;
+    width:  12px;
+    height: 12px;
+    background-color: $carousel-indicator-active-bg;
+  }
+}
+
+// Optional captions
+// -----------------------------
+// Hidden by default for smaller viewports
+.carousel-caption {
+  position: absolute;
+  left: 15%;
+  right: 15%;
+  bottom: 20px;
+  z-index: 10;
+  padding-top: 20px;
+  padding-bottom: 20px;
+  color: $carousel-caption-color;
+  text-align: center;
+  text-shadow: $carousel-text-shadow;
+  & .btn {
+    text-shadow: none; // No shadow for button elements in carousel-caption
+  }
+}
+
+
+// Scale up controls for tablets and up
+@media screen and (min-width: $screen-sm-min) {
+
+  // Scale up the controls a smidge
+  .carousel-control {
+    .glyphicon-chevron-left,
+    .glyphicon-chevron-right,
+    .icon-prev,
+    .icon-next {
+      width: ($carousel-control-font-size * 1.5);
+      height: ($carousel-control-font-size * 1.5);
+      margin-top: ($carousel-control-font-size / -2);
+      font-size: ($carousel-control-font-size * 1.5);
+    }
+    .glyphicon-chevron-left,
+    .icon-prev {
+      margin-left: ($carousel-control-font-size / -2);
+    }
+    .glyphicon-chevron-right,
+    .icon-next {
+      margin-right: ($carousel-control-font-size / -2);
+    }
+  }
+
+  // Show and left align the captions
+  .carousel-caption {
+    left: 20%;
+    right: 20%;
+    padding-bottom: 30px;
+  }
+
+  // Move up the indicators
+  .carousel-indicators {
+    bottom: 20px;
+  }
+}

+ 36 - 0
frontend/sass/vendor/bootstrap/_close.scss

@@ -0,0 +1,36 @@
+//
+// Close icons
+// --------------------------------------------------
+
+
+.close {
+  float: right;
+  font-size: ($font-size-base * 1.5);
+  font-weight: $close-font-weight;
+  line-height: 1;
+  color: $close-color;
+  text-shadow: $close-text-shadow;
+  @include opacity(.2);
+
+  &:hover,
+  &:focus {
+    color: $close-color;
+    text-decoration: none;
+    cursor: pointer;
+    @include opacity(.5);
+  }
+
+  // [converter] extracted button& to button.close
+}
+
+// Additional properties for button version
+// iOS requires the button element instead of an anchor tag.
+// If you want the anchor version, it requires `href="#"`.
+// See https://developer.mozilla.org/en-US/docs/Web/Events/click#Safari_Mobile
+button.close {
+  padding: 0;
+  cursor: pointer;
+  background: transparent;
+  border: 0;
+  -webkit-appearance: none;
+}

+ 69 - 0
frontend/sass/vendor/bootstrap/_code.scss

@@ -0,0 +1,69 @@
+//
+// Code (inline and block)
+// --------------------------------------------------
+
+
+// Inline and block code styles
+code,
+kbd,
+pre,
+samp {
+  font-family: $font-family-monospace;
+}
+
+// Inline code
+code {
+  padding: 2px 4px;
+  font-size: 90%;
+  color: $code-color;
+  background-color: $code-bg;
+  border-radius: $border-radius-base;
+}
+
+// User input typically entered via keyboard
+kbd {
+  padding: 2px 4px;
+  font-size: 90%;
+  color: $kbd-color;
+  background-color: $kbd-bg;
+  border-radius: $border-radius-small;
+  box-shadow: inset 0 -1px 0 rgba(0,0,0,.25);
+
+  kbd {
+    padding: 0;
+    font-size: 100%;
+    font-weight: bold;
+    box-shadow: none;
+  }
+}
+
+// Blocks of code
+pre {
+  display: block;
+  padding: (($line-height-computed - 1) / 2);
+  margin: 0 0 ($line-height-computed / 2);
+  font-size: ($font-size-base - 1); // 14px to 13px
+  line-height: $line-height-base;
+  word-break: break-all;
+  word-wrap: break-word;
+  color: $pre-color;
+  background-color: $pre-bg;
+  border: 1px solid $pre-border-color;
+  border-radius: $border-radius-base;
+
+  // Account for some code outputs that place code tags in pre tags
+  code {
+    padding: 0;
+    font-size: inherit;
+    color: inherit;
+    white-space: pre-wrap;
+    background-color: transparent;
+    border-radius: 0;
+  }
+}
+
+// Enable scrollable blocks of code
+.pre-scrollable {
+  max-height: $pre-scrollable-max-height;
+  overflow-y: scroll;
+}

+ 37 - 0
frontend/sass/vendor/bootstrap/_component-animations.scss

@@ -0,0 +1,37 @@
+//
+// Component animations
+// --------------------------------------------------
+
+// Heads up!
+//
+// We don't use the `.opacity()` mixin here since it causes a bug with text
+// fields in IE7-8. Source: https://github.com/twbs/bootstrap/pull/3552.
+
+.fade {
+  opacity: 0;
+  @include transition(opacity .15s linear);
+  &.in {
+    opacity: 1;
+  }
+}
+
+.collapse {
+  display: none;
+
+  &.in      { display: block; }
+  // [converter] extracted tr&.in to tr.collapse.in
+  // [converter] extracted tbody&.in to tbody.collapse.in
+}
+
+tr.collapse.in    { display: table-row; }
+
+tbody.collapse.in { display: table-row-group; }
+
+.collapsing {
+  position: relative;
+  height: 0;
+  overflow: hidden;
+  @include transition-property(height, visibility);
+  @include transition-duration(.35s);
+  @include transition-timing-function(ease);
+}

+ 216 - 0
frontend/sass/vendor/bootstrap/_dropdowns.scss

@@ -0,0 +1,216 @@
+//
+// Dropdown menus
+// --------------------------------------------------
+
+
+// Dropdown arrow/caret
+.caret {
+  display: inline-block;
+  width: 0;
+  height: 0;
+  margin-left: 2px;
+  vertical-align: middle;
+  border-top:   $caret-width-base dashed;
+  border-top:   $caret-width-base solid \9; // IE8
+  border-right: $caret-width-base solid transparent;
+  border-left:  $caret-width-base solid transparent;
+}
+
+// The dropdown wrapper (div)
+.dropup,
+.dropdown {
+  position: relative;
+}
+
+// Prevent the focus on the dropdown toggle when closing dropdowns
+.dropdown-toggle:focus {
+  outline: 0;
+}
+
+// The dropdown menu (ul)
+.dropdown-menu {
+  position: absolute;
+  top: 100%;
+  left: 0;
+  z-index: $zindex-dropdown;
+  display: none; // none by default, but block on "open" of the menu
+  float: left;
+  min-width: 160px;
+  padding: 5px 0;
+  margin: 2px 0 0; // override default ul
+  list-style: none;
+  font-size: $font-size-base;
+  text-align: left; // Ensures proper alignment if parent has it changed (e.g., modal footer)
+  background-color: $dropdown-bg;
+  border: 1px solid $dropdown-fallback-border; // IE8 fallback
+  border: 1px solid $dropdown-border;
+  border-radius: $border-radius-base;
+  @include box-shadow(0 6px 12px rgba(0,0,0,.175));
+  background-clip: padding-box;
+
+  // Aligns the dropdown menu to right
+  //
+  // Deprecated as of 3.1.0 in favor of `.dropdown-menu-[dir]`
+  &.pull-right {
+    right: 0;
+    left: auto;
+  }
+
+  // Dividers (basically an hr) within the dropdown
+  .divider {
+    @include nav-divider($dropdown-divider-bg);
+  }
+
+  // Links within the dropdown menu
+  > li > a {
+    display: block;
+    padding: 3px 20px;
+    clear: both;
+    font-weight: normal;
+    line-height: $line-height-base;
+    color: $dropdown-link-color;
+    white-space: nowrap; // prevent links from randomly breaking onto new lines
+  }
+}
+
+// Hover/Focus state
+.dropdown-menu > li > a {
+  &:hover,
+  &:focus {
+    text-decoration: none;
+    color: $dropdown-link-hover-color;
+    background-color: $dropdown-link-hover-bg;
+  }
+}
+
+// Active state
+.dropdown-menu > .active > a {
+  &,
+  &:hover,
+  &:focus {
+    color: $dropdown-link-active-color;
+    text-decoration: none;
+    outline: 0;
+    background-color: $dropdown-link-active-bg;
+  }
+}
+
+// Disabled state
+//
+// Gray out text and ensure the hover/focus state remains gray
+
+.dropdown-menu > .disabled > a {
+  &,
+  &:hover,
+  &:focus {
+    color: $dropdown-link-disabled-color;
+  }
+
+  // Nuke hover/focus effects
+  &:hover,
+  &:focus {
+    text-decoration: none;
+    background-color: transparent;
+    background-image: none; // Remove CSS gradient
+    @include reset-filter;
+    cursor: $cursor-disabled;
+  }
+}
+
+// Open state for the dropdown
+.open {
+  // Show the menu
+  > .dropdown-menu {
+    display: block;
+  }
+
+  // Remove the outline when :focus is triggered
+  > a {
+    outline: 0;
+  }
+}
+
+// Menu positioning
+//
+// Add extra class to `.dropdown-menu` to flip the alignment of the dropdown
+// menu with the parent.
+.dropdown-menu-right {
+  left: auto; // Reset the default from `.dropdown-menu`
+  right: 0;
+}
+// With v3, we enabled auto-flipping if you have a dropdown within a right
+// aligned nav component. To enable the undoing of that, we provide an override
+// to restore the default dropdown menu alignment.
+//
+// This is only for left-aligning a dropdown menu within a `.navbar-right` or
+// `.pull-right` nav component.
+.dropdown-menu-left {
+  left: 0;
+  right: auto;
+}
+
+// Dropdown section headers
+.dropdown-header {
+  display: block;
+  padding: 3px 20px;
+  font-size: $font-size-small;
+  line-height: $line-height-base;
+  color: $dropdown-header-color;
+  white-space: nowrap; // as with > li > a
+}
+
+// Backdrop to catch body clicks on mobile, etc.
+.dropdown-backdrop {
+  position: fixed;
+  left: 0;
+  right: 0;
+  bottom: 0;
+  top: 0;
+  z-index: ($zindex-dropdown - 10);
+}
+
+// Right aligned dropdowns
+.pull-right > .dropdown-menu {
+  right: 0;
+  left: auto;
+}
+
+// Allow for dropdowns to go bottom up (aka, dropup-menu)
+//
+// Just add .dropup after the standard .dropdown class and you're set, bro.
+// TODO: abstract this so that the navbar fixed styles are not placed here?
+
+.dropup,
+.navbar-fixed-bottom .dropdown {
+  // Reverse the caret
+  .caret {
+    border-top: 0;
+    border-bottom: $caret-width-base dashed;
+    border-bottom: $caret-width-base solid \9; // IE8
+    content: "";
+  }
+  // Different positioning for bottom up menu
+  .dropdown-menu {
+    top: auto;
+    bottom: 100%;
+    margin-bottom: 2px;
+  }
+}
+
+
+// Component alignment
+//
+// Reiterate per navbar.less and the modified component alignment there.
+
+@media (min-width: $grid-float-breakpoint) {
+  .navbar-right {
+    .dropdown-menu {
+      right: 0; left: auto;
+    }
+    // Necessary for overrides of the default right aligned menu.
+    // Will remove come v4 in all likelihood.
+    .dropdown-menu-left {
+      left: 0; right: auto;
+    }
+  }
+}

+ 617 - 0
frontend/sass/vendor/bootstrap/_forms.scss

@@ -0,0 +1,617 @@
+//
+// Forms
+// --------------------------------------------------
+
+
+// Normalize non-controls
+//
+// Restyle and baseline non-control form elements.
+
+fieldset {
+  padding: 0;
+  margin: 0;
+  border: 0;
+  // Chrome and Firefox set a `min-width: min-content;` on fieldsets,
+  // so we reset that to ensure it behaves more like a standard block element.
+  // See https://github.com/twbs/bootstrap/issues/12359.
+  min-width: 0;
+}
+
+legend {
+  display: block;
+  width: 100%;
+  padding: 0;
+  margin-bottom: $line-height-computed;
+  font-size: ($font-size-base * 1.5);
+  line-height: inherit;
+  color: $legend-color;
+  border: 0;
+  border-bottom: 1px solid $legend-border-color;
+}
+
+label {
+  display: inline-block;
+  max-width: 100%; // Force IE8 to wrap long content (see https://github.com/twbs/bootstrap/issues/13141)
+  margin-bottom: 5px;
+  font-weight: bold;
+}
+
+
+// Normalize form controls
+//
+// While most of our form styles require extra classes, some basic normalization
+// is required to ensure optimum display with or without those classes to better
+// address browser inconsistencies.
+
+// Override content-box in Normalize (* isn't specific enough)
+input[type="search"] {
+  @include box-sizing(border-box);
+}
+
+// Position radios and checkboxes better
+input[type="radio"],
+input[type="checkbox"] {
+  margin: 4px 0 0;
+  margin-top: 1px \9; // IE8-9
+  line-height: normal;
+}
+
+input[type="file"] {
+  display: block;
+}
+
+// Make range inputs behave like textual form controls
+input[type="range"] {
+  display: block;
+  width: 100%;
+}
+
+// Make multiple select elements height not fixed
+select[multiple],
+select[size] {
+  height: auto;
+}
+
+// Focus for file, radio, and checkbox
+input[type="file"]:focus,
+input[type="radio"]:focus,
+input[type="checkbox"]:focus {
+  @include tab-focus;
+}
+
+// Adjust output element
+output {
+  display: block;
+  padding-top: ($padding-base-vertical + 1);
+  font-size: $font-size-base;
+  line-height: $line-height-base;
+  color: $input-color;
+}
+
+
+// Common form controls
+//
+// Shared size and type resets for form controls. Apply `.form-control` to any
+// of the following form controls:
+//
+// select
+// textarea
+// input[type="text"]
+// input[type="password"]
+// input[type="datetime"]
+// input[type="datetime-local"]
+// input[type="date"]
+// input[type="month"]
+// input[type="time"]
+// input[type="week"]
+// input[type="number"]
+// input[type="email"]
+// input[type="url"]
+// input[type="search"]
+// input[type="tel"]
+// input[type="color"]
+
+.form-control {
+  display: block;
+  width: 100%;
+  height: $input-height-base; // Make inputs at least the height of their button counterpart (base line-height + padding + border)
+  padding: $padding-base-vertical $padding-base-horizontal;
+  font-size: $font-size-base;
+  line-height: $line-height-base;
+  color: $input-color;
+  background-color: $input-bg;
+  background-image: none; // Reset unusual Firefox-on-Android default style; see https://github.com/necolas/normalize.css/issues/214
+  border: 1px solid $input-border;
+  border-radius: $input-border-radius; // Note: This has no effect on <select>s in some browsers, due to the limited stylability of <select>s in CSS.
+  @include box-shadow(inset 0 1px 1px rgba(0,0,0,.075));
+  @include transition(border-color ease-in-out .15s, box-shadow ease-in-out .15s);
+
+  // Customize the `:focus` state to imitate native WebKit styles.
+  @include form-control-focus;
+
+  // Placeholder
+  @include placeholder;
+
+  // Unstyle the caret on `<select>`s in IE10+.
+  &::-ms-expand {
+    border: 0;
+    background-color: transparent;
+  }
+
+  // Disabled and read-only inputs
+  //
+  // HTML5 says that controls under a fieldset > legend:first-child won't be
+  // disabled if the fieldset is disabled. Due to implementation difficulty, we
+  // don't honor that edge case; we style them as disabled anyway.
+  &[disabled],
+  &[readonly],
+  fieldset[disabled] & {
+    background-color: $input-bg-disabled;
+    opacity: 1; // iOS fix for unreadable disabled content; see https://github.com/twbs/bootstrap/issues/11655
+  }
+
+  &[disabled],
+  fieldset[disabled] & {
+    cursor: $cursor-disabled;
+  }
+
+  // [converter] extracted textarea& to textarea.form-control
+}
+
+// Reset height for `textarea`s
+textarea.form-control {
+  height: auto;
+}
+
+
+// Search inputs in iOS
+//
+// This overrides the extra rounded corners on search inputs in iOS so that our
+// `.form-control` class can properly style them. Note that this cannot simply
+// be added to `.form-control` as it's not specific enough. For details, see
+// https://github.com/twbs/bootstrap/issues/11586.
+
+input[type="search"] {
+  -webkit-appearance: none;
+}
+
+
+// Special styles for iOS temporal inputs
+//
+// In Mobile Safari, setting `display: block` on temporal inputs causes the
+// text within the input to become vertically misaligned. As a workaround, we
+// set a pixel line-height that matches the given height of the input, but only
+// for Safari. See https://bugs.webkit.org/show_bug.cgi?id=139848
+//
+// Note that as of 9.3, iOS doesn't support `week`.
+
+@media screen and (-webkit-min-device-pixel-ratio: 0) {
+  input[type="date"],
+  input[type="time"],
+  input[type="datetime-local"],
+  input[type="month"] {
+    &.form-control {
+      line-height: $input-height-base;
+    }
+
+    &.input-sm,
+    .input-group-sm & {
+      line-height: $input-height-small;
+    }
+
+    &.input-lg,
+    .input-group-lg & {
+      line-height: $input-height-large;
+    }
+  }
+}
+
+
+// Form groups
+//
+// Designed to help with the organization and spacing of vertical forms. For
+// horizontal forms, use the predefined grid classes.
+
+.form-group {
+  margin-bottom: $form-group-margin-bottom;
+}
+
+
+// Checkboxes and radios
+//
+// Indent the labels to position radios/checkboxes as hanging controls.
+
+.radio,
+.checkbox {
+  position: relative;
+  display: block;
+  margin-top: 10px;
+  margin-bottom: 10px;
+
+  label {
+    min-height: $line-height-computed; // Ensure the input doesn't jump when there is no text
+    padding-left: 20px;
+    margin-bottom: 0;
+    font-weight: normal;
+    cursor: pointer;
+  }
+}
+.radio input[type="radio"],
+.radio-inline input[type="radio"],
+.checkbox input[type="checkbox"],
+.checkbox-inline input[type="checkbox"] {
+  position: absolute;
+  margin-left: -20px;
+  margin-top: 4px \9;
+}
+
+.radio + .radio,
+.checkbox + .checkbox {
+  margin-top: -5px; // Move up sibling radios or checkboxes for tighter spacing
+}
+
+// Radios and checkboxes on same line
+.radio-inline,
+.checkbox-inline {
+  position: relative;
+  display: inline-block;
+  padding-left: 20px;
+  margin-bottom: 0;
+  vertical-align: middle;
+  font-weight: normal;
+  cursor: pointer;
+}
+.radio-inline + .radio-inline,
+.checkbox-inline + .checkbox-inline {
+  margin-top: 0;
+  margin-left: 10px; // space out consecutive inline controls
+}
+
+// Apply same disabled cursor tweak as for inputs
+// Some special care is needed because <label>s don't inherit their parent's `cursor`.
+//
+// Note: Neither radios nor checkboxes can be readonly.
+input[type="radio"],
+input[type="checkbox"] {
+  &[disabled],
+  &.disabled,
+  fieldset[disabled] & {
+    cursor: $cursor-disabled;
+  }
+}
+// These classes are used directly on <label>s
+.radio-inline,
+.checkbox-inline {
+  &.disabled,
+  fieldset[disabled] & {
+    cursor: $cursor-disabled;
+  }
+}
+// These classes are used on elements with <label> descendants
+.radio,
+.checkbox {
+  &.disabled,
+  fieldset[disabled] & {
+    label {
+      cursor: $cursor-disabled;
+    }
+  }
+}
+
+
+// Static form control text
+//
+// Apply class to a `p` element to make any string of text align with labels in
+// a horizontal form layout.
+
+.form-control-static {
+  // Size it appropriately next to real form controls
+  padding-top: ($padding-base-vertical + 1);
+  padding-bottom: ($padding-base-vertical + 1);
+  // Remove default margin from `p`
+  margin-bottom: 0;
+  min-height: ($line-height-computed + $font-size-base);
+
+  &.input-lg,
+  &.input-sm {
+    padding-left: 0;
+    padding-right: 0;
+  }
+}
+
+
+// Form control sizing
+//
+// Build on `.form-control` with modifier classes to decrease or increase the
+// height and font-size of form controls.
+//
+// The `.form-group-* form-control` variations are sadly duplicated to avoid the
+// issue documented in https://github.com/twbs/bootstrap/issues/15074.
+
+@include input-size('.input-sm', $input-height-small, $padding-small-vertical, $padding-small-horizontal, $font-size-small, $line-height-small, $input-border-radius-small);
+.form-group-sm {
+  .form-control {
+    height: $input-height-small;
+    padding: $padding-small-vertical $padding-small-horizontal;
+    font-size: $font-size-small;
+    line-height: $line-height-small;
+    border-radius: $input-border-radius-small;
+  }
+  select.form-control {
+    height: $input-height-small;
+    line-height: $input-height-small;
+  }
+  textarea.form-control,
+  select[multiple].form-control {
+    height: auto;
+  }
+  .form-control-static {
+    height: $input-height-small;
+    min-height: ($line-height-computed + $font-size-small);
+    padding: ($padding-small-vertical + 1) $padding-small-horizontal;
+    font-size: $font-size-small;
+    line-height: $line-height-small;
+  }
+}
+
+@include input-size('.input-lg', $input-height-large, $padding-large-vertical, $padding-large-horizontal, $font-size-large, $line-height-large, $input-border-radius-large);
+.form-group-lg {
+  .form-control {
+    height: $input-height-large;
+    padding: $padding-large-vertical $padding-large-horizontal;
+    font-size: $font-size-large;
+    line-height: $line-height-large;
+    border-radius: $input-border-radius-large;
+  }
+  select.form-control {
+    height: $input-height-large;
+    line-height: $input-height-large;
+  }
+  textarea.form-control,
+  select[multiple].form-control {
+    height: auto;
+  }
+  .form-control-static {
+    height: $input-height-large;
+    min-height: ($line-height-computed + $font-size-large);
+    padding: ($padding-large-vertical + 1) $padding-large-horizontal;
+    font-size: $font-size-large;
+    line-height: $line-height-large;
+  }
+}
+
+
+// Form control feedback states
+//
+// Apply contextual and semantic states to individual form controls.
+
+.has-feedback {
+  // Enable absolute positioning
+  position: relative;
+
+  // Ensure icons don't overlap text
+  .form-control {
+    padding-right: ($input-height-base * 1.25);
+  }
+}
+// Feedback icon (requires .glyphicon classes)
+.form-control-feedback {
+  position: absolute;
+  top: 0;
+  right: 0;
+  z-index: 2; // Ensure icon is above input groups
+  display: block;
+  width: $input-height-base;
+  height: $input-height-base;
+  line-height: $input-height-base;
+  text-align: center;
+  pointer-events: none;
+}
+.input-lg + .form-control-feedback,
+.input-group-lg + .form-control-feedback,
+.form-group-lg .form-control + .form-control-feedback {
+  width: $input-height-large;
+  height: $input-height-large;
+  line-height: $input-height-large;
+}
+.input-sm + .form-control-feedback,
+.input-group-sm + .form-control-feedback,
+.form-group-sm .form-control + .form-control-feedback {
+  width: $input-height-small;
+  height: $input-height-small;
+  line-height: $input-height-small;
+}
+
+// Feedback states
+.has-success {
+  @include form-control-validation($state-success-text, $state-success-text, $state-success-bg);
+}
+.has-warning {
+  @include form-control-validation($state-warning-text, $state-warning-text, $state-warning-bg);
+}
+.has-error {
+  @include form-control-validation($state-danger-text, $state-danger-text, $state-danger-bg);
+}
+
+// Reposition feedback icon if input has visible label above
+.has-feedback label {
+
+  & ~ .form-control-feedback {
+    top: ($line-height-computed + 5); // Height of the `label` and its margin
+  }
+  &.sr-only ~ .form-control-feedback {
+    top: 0;
+  }
+}
+
+
+// Help text
+//
+// Apply to any element you wish to create light text for placement immediately
+// below a form control. Use for general help, formatting, or instructional text.
+
+.help-block {
+  display: block; // account for any element using help-block
+  margin-top: 5px;
+  margin-bottom: 10px;
+  color: lighten($text-color, 25%); // lighten the text some for contrast
+}
+
+
+// Inline forms
+//
+// Make forms appear inline(-block) by adding the `.form-inline` class. Inline
+// forms begin stacked on extra small (mobile) devices and then go inline when
+// viewports reach <768px.
+//
+// Requires wrapping inputs and labels with `.form-group` for proper display of
+// default HTML form controls and our custom form controls (e.g., input groups).
+//
+// Heads up! This is mixin-ed into `.navbar-form` in navbars.less.
+
+// [converter] extracted from `.form-inline` for libsass compatibility
+@mixin form-inline {
+
+  // Kick in the inline
+  @media (min-width: $screen-sm-min) {
+    // Inline-block all the things for "inline"
+    .form-group {
+      display: inline-block;
+      margin-bottom: 0;
+      vertical-align: middle;
+    }
+
+    // In navbar-form, allow folks to *not* use `.form-group`
+    .form-control {
+      display: inline-block;
+      width: auto; // Prevent labels from stacking above inputs in `.form-group`
+      vertical-align: middle;
+    }
+
+    // Make static controls behave like regular ones
+    .form-control-static {
+      display: inline-block;
+    }
+
+    .input-group {
+      display: inline-table;
+      vertical-align: middle;
+
+      .input-group-addon,
+      .input-group-btn,
+      .form-control {
+        width: auto;
+      }
+    }
+
+    // Input groups need that 100% width though
+    .input-group > .form-control {
+      width: 100%;
+    }
+
+    .control-label {
+      margin-bottom: 0;
+      vertical-align: middle;
+    }
+
+    // Remove default margin on radios/checkboxes that were used for stacking, and
+    // then undo the floating of radios and checkboxes to match.
+    .radio,
+    .checkbox {
+      display: inline-block;
+      margin-top: 0;
+      margin-bottom: 0;
+      vertical-align: middle;
+
+      label {
+        padding-left: 0;
+      }
+    }
+    .radio input[type="radio"],
+    .checkbox input[type="checkbox"] {
+      position: relative;
+      margin-left: 0;
+    }
+
+    // Re-override the feedback icon.
+    .has-feedback .form-control-feedback {
+      top: 0;
+    }
+  }
+}
+// [converter] extracted as `@mixin form-inline` for libsass compatibility
+.form-inline {
+  @include form-inline;
+}
+
+
+
+// Horizontal forms
+//
+// Horizontal forms are built on grid classes and allow you to create forms with
+// labels on the left and inputs on the right.
+
+.form-horizontal {
+
+  // Consistent vertical alignment of radios and checkboxes
+  //
+  // Labels also get some reset styles, but that is scoped to a media query below.
+  .radio,
+  .checkbox,
+  .radio-inline,
+  .checkbox-inline {
+    margin-top: 0;
+    margin-bottom: 0;
+    padding-top: ($padding-base-vertical + 1); // Default padding plus a border
+  }
+  // Account for padding we're adding to ensure the alignment and of help text
+  // and other content below items
+  .radio,
+  .checkbox {
+    min-height: ($line-height-computed + ($padding-base-vertical + 1));
+  }
+
+  // Make form groups behave like rows
+  .form-group {
+    @include make-row;
+  }
+
+  // Reset spacing and right align labels, but scope to media queries so that
+  // labels on narrow viewports stack the same as a default form example.
+  @media (min-width: $screen-sm-min) {
+    .control-label {
+      text-align: right;
+      margin-bottom: 0;
+      padding-top: ($padding-base-vertical + 1); // Default padding plus a border
+    }
+  }
+
+  // Validation states
+  //
+  // Reposition the icon because it's now within a grid column and columns have
+  // `position: relative;` on them. Also accounts for the grid gutter padding.
+  .has-feedback .form-control-feedback {
+    right: floor(($grid-gutter-width / 2));
+  }
+
+  // Form group sizes
+  //
+  // Quick utility class for applying `.input-lg` and `.input-sm` styles to the
+  // inputs and labels within a `.form-group`.
+  .form-group-lg {
+    @media (min-width: $screen-sm-min) {
+      .control-label {
+        padding-top: ($padding-large-vertical + 1);
+        font-size: $font-size-large;
+      }
+    }
+  }
+  .form-group-sm {
+    @media (min-width: $screen-sm-min) {
+      .control-label {
+        padding-top: ($padding-small-vertical + 1);
+        font-size: $font-size-small;
+      }
+    }
+  }
+}

+ 307 - 0
frontend/sass/vendor/bootstrap/_glyphicons.scss

@@ -0,0 +1,307 @@
+//
+// Glyphicons for Bootstrap
+//
+// Since icons are fonts, they can be placed anywhere text is placed and are
+// thus automatically sized to match the surrounding child. To use, create an
+// inline element with the appropriate classes, like so:
+//
+// <a href="#"><span class="glyphicon glyphicon-star"></span> Star</a>
+
+@at-root {
+  // Import the fonts
+  @font-face {
+    font-family: 'Glyphicons Halflings';
+    src: url(if($bootstrap-sass-asset-helper, twbs-font-path('#{$icon-font-path}#{$icon-font-name}.eot'), '#{$icon-font-path}#{$icon-font-name}.eot'));
+    src: url(if($bootstrap-sass-asset-helper, twbs-font-path('#{$icon-font-path}#{$icon-font-name}.eot?#iefix'), '#{$icon-font-path}#{$icon-font-name}.eot?#iefix')) format('embedded-opentype'),
+         url(if($bootstrap-sass-asset-helper, twbs-font-path('#{$icon-font-path}#{$icon-font-name}.woff2'), '#{$icon-font-path}#{$icon-font-name}.woff2')) format('woff2'),
+         url(if($bootstrap-sass-asset-helper, twbs-font-path('#{$icon-font-path}#{$icon-font-name}.woff'), '#{$icon-font-path}#{$icon-font-name}.woff')) format('woff'),
+         url(if($bootstrap-sass-asset-helper, twbs-font-path('#{$icon-font-path}#{$icon-font-name}.ttf'), '#{$icon-font-path}#{$icon-font-name}.ttf')) format('truetype'),
+         url(if($bootstrap-sass-asset-helper, twbs-font-path('#{$icon-font-path}#{$icon-font-name}.svg##{$icon-font-svg-id}'), '#{$icon-font-path}#{$icon-font-name}.svg##{$icon-font-svg-id}')) format('svg');
+  }
+}
+
+// Catchall baseclass
+.glyphicon {
+  position: relative;
+  top: 1px;
+  display: inline-block;
+  font-family: 'Glyphicons Halflings';
+  font-style: normal;
+  font-weight: normal;
+  line-height: 1;
+  -webkit-font-smoothing: antialiased;
+  -moz-osx-font-smoothing: grayscale;
+}
+
+// Individual icons
+.glyphicon-asterisk               { &:before { content: "\002a"; } }
+.glyphicon-plus                   { &:before { content: "\002b"; } }
+.glyphicon-euro,
+.glyphicon-eur                    { &:before { content: "\20ac"; } }
+.glyphicon-minus                  { &:before { content: "\2212"; } }
+.glyphicon-cloud                  { &:before { content: "\2601"; } }
+.glyphicon-envelope               { &:before { content: "\2709"; } }
+.glyphicon-pencil                 { &:before { content: "\270f"; } }
+.glyphicon-glass                  { &:before { content: "\e001"; } }
+.glyphicon-music                  { &:before { content: "\e002"; } }
+.glyphicon-search                 { &:before { content: "\e003"; } }
+.glyphicon-heart                  { &:before { content: "\e005"; } }