├── .babelrc
├── .editorconfig
├── .env.example
├── .eslintignore
├── .eslintrc.js
├── .firebaserc
├── .gitignore
├── .postcssrc.js
├── README.md
├── build
├── build.js
├── check-versions.js
├── dev-client.js
├── dev-server.js
├── utils.js
├── vue-loader.conf.js
├── webpack.base.conf.js
├── webpack.dev.conf.js
└── webpack.prod.conf.js
├── config
├── dev.env.js
├── index.js
└── prod.env.js
├── database.rules.json
├── firebase.json
├── index.html
├── package.json
├── src
├── App.vue
├── assets
│ ├── css
│ │ └── style.css
│ ├── img
│ │ ├── arrow-profile.svg
│ │ └── vueschool-logo.svg
│ └── logo.png
├── components
│ ├── AppDate.vue
│ ├── AppSpinner.vue
│ ├── CategoryList.vue
│ ├── CategoryListItem.vue
│ ├── ForumList.vue
│ ├── ForumListItem.vue
│ ├── PostEditor.vue
│ ├── PostList.vue
│ ├── PostListItem.vue
│ ├── TheNavbar.vue
│ ├── ThreadEditor.vue
│ ├── ThreadList.vue
│ ├── ThreadListItem.vue
│ ├── UserProfileCard.vue
│ └── UserProfileCardEditor.vue
├── data.json
├── directives
│ ├── click-outside.js
│ └── handle-scroll.js
├── main.js
├── mixins
│ └── asyncDataStatus.js
├── pages
│ ├── PageCategory.vue
│ ├── PageForum.vue
│ ├── PageHome.vue
│ ├── PageNotFound.vue
│ ├── PageProfile.vue
│ ├── PageRegister.vue
│ ├── PageSignIn.vue
│ ├── PageThreadCreate.vue
│ ├── PageThreadEdit.vue
│ └── PageThreadShow.vue
├── router
│ └── index.js
├── store
│ ├── actions.js
│ ├── assetHelpers.js
│ ├── getters.js
│ ├── index.js
│ ├── modules
│ │ ├── auth.js
│ │ ├── categories.js
│ │ ├── forums.js
│ │ ├── posts.js
│ │ ├── threads.js
│ │ └── users.js
│ └── mutations.js
└── utils
│ ├── index.js
│ └── validators.js
├── static
└── .gitkeep
└── yarn.lock
/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": [
3 | ["env", {
4 | "modules": false,
5 | "targets": {
6 | "browsers": ["> 1%", "last 2 versions", "not ie <= 8"]
7 | }
8 | }],
9 | "stage-2"
10 | ],
11 | "plugins": ["transform-runtime"],
12 | "env": {
13 | "test": {
14 | "presets": ["env", "stage-2"],
15 | "plugins": ["istanbul"]
16 | }
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | root = true
2 |
3 | [*]
4 | charset = utf-8
5 | indent_style = space
6 | indent_size = 2
7 | end_of_line = lf
8 | insert_final_newline = true
9 | trim_trailing_whitespace = true
10 |
--------------------------------------------------------------------------------
/.env.example:
--------------------------------------------------------------------------------
1 | FIREBASE_API_KEY=AIzaSyC_WLhTuqbKU3unweXosx8zsKQccBOKs7c
2 | FIREBASE_AUTH_DOMAIN=vue-school-forum.firebaseapp.com
3 | FIREBASE_DATABASE_URL=https://vue-school-forum.firebaseio.com
4 | FIREBASE_PROJECT_ID=vue-school-forum
5 | FIREBASE_STORAGE_BUCKET=vue-school-forum.appspot.com
6 | FIREBASE_MESSAGING_ID=426987850952
7 |
--------------------------------------------------------------------------------
/.eslintignore:
--------------------------------------------------------------------------------
1 | build/*.js
2 | config/*.js
3 |
--------------------------------------------------------------------------------
/.eslintrc.js:
--------------------------------------------------------------------------------
1 | // https://eslint.org/docs/user-guide/configuring
2 |
3 | module.exports = {
4 | root: true,
5 | parser: 'babel-eslint',
6 | parserOptions: {
7 | sourceType: 'module'
8 | },
9 | env: {
10 | browser: true,
11 | },
12 | // https://github.com/standard/standard/blob/master/docs/RULES-en.md
13 | extends: 'standard',
14 | // required to lint *.vue files
15 | plugins: [
16 | 'html'
17 | ],
18 | // add your custom rules here
19 | 'rules': {
20 | // allow paren-less arrow functions
21 | 'arrow-parens': 0,
22 | // allow async-await
23 | 'generator-star-spacing': 0,
24 | // allow debugger during development
25 | 'no-debugger': process.env.NODE_ENV === 'production' ? 2 : 0,
26 | "no-unused-vars": process.env.NODE_ENV === 'production' ? 2 : 1
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/.firebaserc:
--------------------------------------------------------------------------------
1 | {
2 | "projects": {
3 | "default": "vue-school-forum"
4 | }
5 | }
6 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | node_modules/
3 | dist/
4 | npm-debug.log*
5 | yarn-debug.log*
6 | yarn-error.log*
7 |
8 | .env
9 |
10 | # Editor directories and files
11 | .idea
12 | .vscode
13 | *.suo
14 | *.ntvs*
15 | *.njsproj
16 | *.sln
17 |
--------------------------------------------------------------------------------
/.postcssrc.js:
--------------------------------------------------------------------------------
1 | // https://github.com/michael-ciniawsky/postcss-load-config
2 |
3 | module.exports = {
4 | "plugins": {
5 | // to edit target browsers: use "browserslist" field in package.json
6 | "autoprefixer": {}
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # vue-master-class
2 |
3 | This repository contains the source code of [The Vue.js Master Class](https://vueschool.io).
4 |
5 | ## The Vue.js Master Class
6 |
7 | The goal of the Master Class is to teach you Vue.js along with Best Practices, Modern Javascript, and other exciting technologies, by building a Real World application - a forum.
8 |
9 | ## We cover the fundamentals, like
10 |
11 | - Vue cli, router and State management with Vuex
12 | - Modern Javascript (ES6/7/8)
13 | - User permissions & protected routes
14 | - Third party authentication
15 | - Firebase Realtime Database & Cloud functions
16 | - Automatic code review with ESLint
17 | - Deployment
18 | - Application architecture and best practices
19 |
20 | ## We also dive into harder topics, like:
21 |
22 | - Higher Order Functions
23 | - Creating Vue Plugins
24 | - Code Splitting
25 | - Support for older Browsers
26 | - Webpack configuration
27 | - SEO and pre-rendering
28 | - Reactive programming with RxJS
29 |
30 | By completing the Vue.js Master Class, you will be able to land any Vue related job or optimize/improve your own projects!
31 |
32 | Convinced? [Enroll now](https://vueschool.io/the-vuejs-master-class)
33 |
34 |
35 |
36 | ## Build Setup
37 |
38 | ``` bash
39 | # install dependencies
40 | yarn
41 |
42 | # serve with hot reload at localhost:8080
43 | yarn dev
44 |
45 | # build for production with minification
46 | yarn build
47 |
48 | # build for production and view the bundle analyzer report
49 | npm run build --report
50 | ```
51 |
52 | For a detailed explanation on how things work, check out the [guide](http://vuejs-templates.github.io/webpack/) and [docs for vue-loader](http://vuejs.github.io/vue-loader).
53 |
--------------------------------------------------------------------------------
/build/build.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 | require('./check-versions')()
3 |
4 | process.env.NODE_ENV = 'production'
5 |
6 | const ora = require('ora')
7 | const rm = require('rimraf')
8 | const path = require('path')
9 | const chalk = require('chalk')
10 | const webpack = require('webpack')
11 | const config = require('../config')
12 | const webpackConfig = require('./webpack.prod.conf')
13 |
14 | const spinner = ora('building for production...')
15 | spinner.start()
16 |
17 | rm(path.join(config.build.assetsRoot, config.build.assetsSubDirectory), err => {
18 | if (err) throw err
19 | webpack(webpackConfig, function (err, stats) {
20 | spinner.stop()
21 | if (err) throw err
22 | process.stdout.write(stats.toString({
23 | colors: true,
24 | modules: false,
25 | children: false,
26 | chunks: false,
27 | chunkModules: false
28 | }) + '\n\n')
29 |
30 | if (stats.hasErrors()) {
31 | console.log(chalk.red(' Build failed with errors.\n'))
32 | process.exit(1)
33 | }
34 |
35 | console.log(chalk.cyan(' Build complete.\n'))
36 | console.log(chalk.yellow(
37 | ' Tip: built files are meant to be served over an HTTP server.\n' +
38 | ' Opening index.html over file:// won\'t work.\n'
39 | ))
40 | })
41 | })
42 |
--------------------------------------------------------------------------------
/build/check-versions.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 | const chalk = require('chalk')
3 | const semver = require('semver')
4 | const packageConfig = require('../package.json')
5 | const shell = require('shelljs')
6 | function exec (cmd) {
7 | return require('child_process').execSync(cmd).toString().trim()
8 | }
9 |
10 | const versionRequirements = [
11 | {
12 | name: 'node',
13 | currentVersion: semver.clean(process.version),
14 | versionRequirement: packageConfig.engines.node
15 | }
16 | ]
17 |
18 | if (shell.which('npm')) {
19 | versionRequirements.push({
20 | name: 'npm',
21 | currentVersion: exec('npm --version'),
22 | versionRequirement: packageConfig.engines.npm
23 | })
24 | }
25 |
26 | module.exports = function () {
27 | const warnings = []
28 | for (let i = 0; i < versionRequirements.length; i++) {
29 | const mod = versionRequirements[i]
30 | if (!semver.satisfies(mod.currentVersion, mod.versionRequirement)) {
31 | warnings.push(mod.name + ': ' +
32 | chalk.red(mod.currentVersion) + ' should be ' +
33 | chalk.green(mod.versionRequirement)
34 | )
35 | }
36 | }
37 |
38 | if (warnings.length) {
39 | console.log('')
40 | console.log(chalk.yellow('To use this template, you must update following to modules:'))
41 | console.log()
42 | for (let i = 0; i < warnings.length; i++) {
43 | const warning = warnings[i]
44 | console.log(' ' + warning)
45 | }
46 | console.log()
47 | process.exit(1)
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/build/dev-client.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable */
2 | 'use strict'
3 | require('eventsource-polyfill')
4 | var hotClient = require('webpack-hot-middleware/client?noInfo=true&reload=true')
5 |
6 | hotClient.subscribe(function (event) {
7 | if (event.action === 'reload') {
8 | window.location.reload()
9 | }
10 | })
11 |
--------------------------------------------------------------------------------
/build/dev-server.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 | require('./check-versions')()
3 |
4 | const config = require('../config')
5 | if (!process.env.NODE_ENV) {
6 | process.env.NODE_ENV = JSON.parse(config.dev.env.NODE_ENV)
7 | }
8 |
9 | const opn = require('opn')
10 | const path = require('path')
11 | const express = require('express')
12 | const webpack = require('webpack')
13 | const proxyMiddleware = require('http-proxy-middleware')
14 | const webpackConfig = require('./webpack.dev.conf')
15 |
16 | // default port where dev server listens for incoming traffic
17 | const port = process.env.PORT || config.dev.port
18 | // automatically open browser, if not set will be false
19 | const autoOpenBrowser = !!config.dev.autoOpenBrowser
20 | // Define HTTP proxies to your custom API backend
21 | // https://github.com/chimurai/http-proxy-middleware
22 | const proxyTable = config.dev.proxyTable
23 |
24 | const app = express()
25 | const compiler = webpack(webpackConfig)
26 |
27 | const devMiddleware = require('webpack-dev-middleware')(compiler, {
28 | publicPath: webpackConfig.output.publicPath,
29 | quiet: true
30 | })
31 |
32 | const hotMiddleware = require('webpack-hot-middleware')(compiler, {
33 | log: false,
34 | heartbeat: 2000
35 | })
36 | // force page reload when html-webpack-plugin template changes
37 | // currently disabled until this is resolved:
38 | // https://github.com/jantimon/html-webpack-plugin/issues/680
39 | // compiler.plugin('compilation', function (compilation) {
40 | // compilation.plugin('html-webpack-plugin-after-emit', function (data, cb) {
41 | // hotMiddleware.publish({ action: 'reload' })
42 | // cb()
43 | // })
44 | // })
45 |
46 | // enable hot-reload and state-preserving
47 | // compilation error display
48 | app.use(hotMiddleware)
49 |
50 | // proxy api requests
51 | Object.keys(proxyTable).forEach(function (context) {
52 | let options = proxyTable[context]
53 | if (typeof options === 'string') {
54 | options = { target: options }
55 | }
56 | app.use(proxyMiddleware(options.filter || context, options))
57 | })
58 |
59 | // handle fallback for HTML5 history API
60 | app.use(require('connect-history-api-fallback')())
61 |
62 | // serve webpack bundle output
63 | app.use(devMiddleware)
64 |
65 | // serve pure static assets
66 | const staticPath = path.posix.join(config.dev.assetsPublicPath, config.dev.assetsSubDirectory)
67 | app.use(staticPath, express.static('./static'))
68 |
69 | const uri = 'http://localhost:' + port
70 |
71 | var _resolve
72 | var _reject
73 | var readyPromise = new Promise((resolve, reject) => {
74 | _resolve = resolve
75 | _reject = reject
76 | })
77 |
78 | var server
79 | var portfinder = require('portfinder')
80 | portfinder.basePort = port
81 |
82 | console.log('> Starting dev server...')
83 | devMiddleware.waitUntilValid(() => {
84 | portfinder.getPort((err, port) => {
85 | if (err) {
86 | _reject(err)
87 | }
88 | process.env.PORT = port
89 | var uri = 'http://localhost:' + port
90 | console.log('> Listening at ' + uri + '\n')
91 | // when env is testing, don't need open it
92 | if (autoOpenBrowser && process.env.NODE_ENV !== 'testing') {
93 | opn(uri)
94 | }
95 | server = app.listen(port)
96 | _resolve()
97 | })
98 | })
99 |
100 | module.exports = {
101 | ready: readyPromise,
102 | close: () => {
103 | server.close()
104 | }
105 | }
106 |
--------------------------------------------------------------------------------
/build/utils.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 | const path = require('path')
3 | const config = require('../config')
4 | const ExtractTextPlugin = require('extract-text-webpack-plugin')
5 |
6 | exports.assetsPath = function (_path) {
7 | const assetsSubDirectory = process.env.NODE_ENV === 'production'
8 | ? config.build.assetsSubDirectory
9 | : config.dev.assetsSubDirectory
10 | return path.posix.join(assetsSubDirectory, _path)
11 | }
12 |
13 | exports.cssLoaders = function (options) {
14 | options = options || {}
15 |
16 | const cssLoader = {
17 | loader: 'css-loader',
18 | options: {
19 | minimize: process.env.NODE_ENV === 'production',
20 | sourceMap: options.sourceMap
21 | }
22 | }
23 |
24 | // generate loader string to be used with extract text plugin
25 | function generateLoaders (loader, loaderOptions) {
26 | const loaders = [cssLoader]
27 | if (loader) {
28 | loaders.push({
29 | loader: loader + '-loader',
30 | options: Object.assign({}, loaderOptions, {
31 | sourceMap: options.sourceMap
32 | })
33 | })
34 | }
35 |
36 | // Extract CSS when that option is specified
37 | // (which is the case during production build)
38 | if (options.extract) {
39 | return ExtractTextPlugin.extract({
40 | use: loaders,
41 | fallback: 'vue-style-loader'
42 | })
43 | } else {
44 | return ['vue-style-loader'].concat(loaders)
45 | }
46 | }
47 |
48 | // https://vue-loader.vuejs.org/en/configurations/extract-css.html
49 | return {
50 | css: generateLoaders(),
51 | postcss: generateLoaders(),
52 | less: generateLoaders('less'),
53 | sass: generateLoaders('sass', { indentedSyntax: true }),
54 | scss: generateLoaders('sass'),
55 | stylus: generateLoaders('stylus'),
56 | styl: generateLoaders('stylus')
57 | }
58 | }
59 |
60 | // Generate loaders for standalone style files (outside of .vue)
61 | exports.styleLoaders = function (options) {
62 | const output = []
63 | const loaders = exports.cssLoaders(options)
64 | for (const extension in loaders) {
65 | const loader = loaders[extension]
66 | output.push({
67 | test: new RegExp('\\.' + extension + '$'),
68 | use: loader
69 | })
70 | }
71 | return output
72 | }
73 |
--------------------------------------------------------------------------------
/build/vue-loader.conf.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 | const utils = require('./utils')
3 | const config = require('../config')
4 | const isProduction = process.env.NODE_ENV === 'production'
5 |
6 | module.exports = {
7 | loaders: utils.cssLoaders({
8 | sourceMap: isProduction
9 | ? config.build.productionSourceMap
10 | : config.dev.cssSourceMap,
11 | extract: isProduction
12 | }),
13 | transformToRequire: {
14 | video: 'src',
15 | source: 'src',
16 | img: 'src',
17 | image: 'xlink:href'
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/build/webpack.base.conf.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 | const path = require('path')
3 | const utils = require('./utils')
4 | const config = require('../config')
5 | const vueLoaderConfig = require('./vue-loader.conf')
6 |
7 | function resolve (dir) {
8 | return path.join(__dirname, '..', dir)
9 | }
10 |
11 | module.exports = {
12 | entry: {
13 | app: './src/main.js'
14 | },
15 | output: {
16 | path: config.build.assetsRoot,
17 | filename: '[name].js',
18 | publicPath: process.env.NODE_ENV === 'production'
19 | ? config.build.assetsPublicPath
20 | : config.dev.assetsPublicPath
21 | },
22 | resolve: {
23 | extensions: ['.js', '.vue', '.json'],
24 | alias: {
25 | 'vue$': 'vue/dist/vue.esm.js',
26 | '@': resolve('src'),
27 | }
28 | },
29 | module: {
30 | rules: [
31 | {
32 | test: /\.(js|vue)$/,
33 | loader: 'eslint-loader',
34 | enforce: 'pre',
35 | include: [resolve('src'), resolve('test')],
36 | options: {
37 | formatter: require('eslint-friendly-formatter')
38 | }
39 | },
40 | {
41 | test: /\.vue$/,
42 | loader: 'vue-loader',
43 | options: vueLoaderConfig
44 | },
45 | {
46 | test: /\.js$/,
47 | loader: 'babel-loader',
48 | include: [resolve('src'), resolve('test')]
49 | },
50 | {
51 | test: /\.(png|jpe?g|gif|svg)(\?.*)?$/,
52 | loader: 'url-loader',
53 | options: {
54 | limit: 10000,
55 | name: utils.assetsPath('img/[name].[hash:7].[ext]')
56 | }
57 | },
58 | {
59 | test: /\.(mp4|webm|ogg|mp3|wav|flac|aac)(\?.*)?$/,
60 | loader: 'url-loader',
61 | options: {
62 | limit: 10000,
63 | name: utils.assetsPath('media/[name].[hash:7].[ext]')
64 | }
65 | },
66 | {
67 | test: /\.(woff2?|eot|ttf|otf)(\?.*)?$/,
68 | loader: 'url-loader',
69 | options: {
70 | limit: 10000,
71 | name: utils.assetsPath('fonts/[name].[hash:7].[ext]')
72 | }
73 | }
74 | ]
75 | }
76 | }
77 |
--------------------------------------------------------------------------------
/build/webpack.dev.conf.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 | const utils = require('./utils')
3 | const webpack = require('webpack')
4 | const config = require('../config')
5 | const merge = require('webpack-merge')
6 | const baseWebpackConfig = require('./webpack.base.conf')
7 | const HtmlWebpackPlugin = require('html-webpack-plugin')
8 | const FriendlyErrorsPlugin = require('friendly-errors-webpack-plugin')
9 |
10 | // add hot-reload related code to entry chunks
11 | Object.keys(baseWebpackConfig.entry).forEach(function (name) {
12 | baseWebpackConfig.entry[name] = ['./build/dev-client'].concat(baseWebpackConfig.entry[name])
13 | })
14 |
15 | module.exports = merge(baseWebpackConfig, {
16 | module: {
17 | rules: utils.styleLoaders({ sourceMap: config.dev.cssSourceMap })
18 | },
19 | // cheap-module-eval-source-map is faster for development
20 | devtool: '#cheap-module-eval-source-map',
21 | plugins: [
22 | new webpack.DefinePlugin({
23 | 'process.env': config.dev.env
24 | }),
25 | // https://github.com/glenjamin/webpack-hot-middleware#installation--usage
26 | new webpack.HotModuleReplacementPlugin(),
27 | new webpack.NoEmitOnErrorsPlugin(),
28 | // https://github.com/ampedandwired/html-webpack-plugin
29 | new HtmlWebpackPlugin({
30 | filename: 'index.html',
31 | template: 'index.html',
32 | inject: true
33 | }),
34 | new FriendlyErrorsPlugin()
35 | ]
36 | })
37 |
--------------------------------------------------------------------------------
/build/webpack.prod.conf.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 | const path = require('path')
3 | const utils = require('./utils')
4 | const webpack = require('webpack')
5 | const config = require('../config')
6 | const merge = require('webpack-merge')
7 | const baseWebpackConfig = require('./webpack.base.conf')
8 | const CopyWebpackPlugin = require('copy-webpack-plugin')
9 | const HtmlWebpackPlugin = require('html-webpack-plugin')
10 | const ExtractTextPlugin = require('extract-text-webpack-plugin')
11 | const OptimizeCSSPlugin = require('optimize-css-assets-webpack-plugin')
12 |
13 | const env = config.build.env
14 |
15 | const webpackConfig = merge(baseWebpackConfig, {
16 | module: {
17 | rules: utils.styleLoaders({
18 | sourceMap: config.build.productionSourceMap,
19 | extract: true
20 | })
21 | },
22 | devtool: config.build.productionSourceMap ? '#source-map' : false,
23 | output: {
24 | path: config.build.assetsRoot,
25 | filename: utils.assetsPath('js/[name].[chunkhash].js'),
26 | chunkFilename: utils.assetsPath('js/[id].[chunkhash].js')
27 | },
28 | plugins: [
29 | // http://vuejs.github.io/vue-loader/en/workflow/production.html
30 | new webpack.DefinePlugin({
31 | 'process.env': env
32 | }),
33 | // UglifyJs do not support ES6+, you can also use babel-minify for better treeshaking: https://github.com/babel/minify
34 | new webpack.optimize.UglifyJsPlugin({
35 | compress: {
36 | warnings: false
37 | },
38 | sourceMap: true
39 | }),
40 | // extract css into its own file
41 | new ExtractTextPlugin({
42 | filename: utils.assetsPath('css/[name].[contenthash].css')
43 | }),
44 | // Compress extracted CSS. We are using this plugin so that possible
45 | // duplicated CSS from different components can be deduped.
46 | new OptimizeCSSPlugin({
47 | cssProcessorOptions: {
48 | safe: true
49 | }
50 | }),
51 | // generate dist index.html with correct asset hash for caching.
52 | // you can customize output by editing /index.html
53 | // see https://github.com/ampedandwired/html-webpack-plugin
54 | new HtmlWebpackPlugin({
55 | filename: config.build.index,
56 | template: 'index.html',
57 | inject: true,
58 | minify: {
59 | removeComments: true,
60 | collapseWhitespace: true,
61 | removeAttributeQuotes: true
62 | // more options:
63 | // https://github.com/kangax/html-minifier#options-quick-reference
64 | },
65 | // necessary to consistently work with multiple chunks via CommonsChunkPlugin
66 | chunksSortMode: 'dependency'
67 | }),
68 | // keep module.id stable when vender modules does not change
69 | new webpack.HashedModuleIdsPlugin(),
70 | // split vendor js into its own file
71 | new webpack.optimize.CommonsChunkPlugin({
72 | name: 'vendor',
73 | minChunks: function (module) {
74 | // any required modules inside node_modules are extracted to vendor
75 | return (
76 | module.resource &&
77 | /\.js$/.test(module.resource) &&
78 | module.resource.indexOf(
79 | path.join(__dirname, '../node_modules')
80 | ) === 0
81 | )
82 | }
83 | }),
84 | // extract webpack runtime and module manifest to its own file in order to
85 | // prevent vendor hash from being updated whenever app bundle is updated
86 | new webpack.optimize.CommonsChunkPlugin({
87 | name: 'manifest',
88 | chunks: ['vendor']
89 | }),
90 | // copy custom static assets
91 | new CopyWebpackPlugin([
92 | {
93 | from: path.resolve(__dirname, '../static'),
94 | to: config.build.assetsSubDirectory,
95 | ignore: ['.*']
96 | }
97 | ])
98 | ]
99 | })
100 |
101 | if (config.build.productionGzip) {
102 | const CompressionWebpackPlugin = require('compression-webpack-plugin')
103 |
104 | webpackConfig.plugins.push(
105 | new CompressionWebpackPlugin({
106 | asset: '[path].gz[query]',
107 | algorithm: 'gzip',
108 | test: new RegExp(
109 | '\\.(' +
110 | config.build.productionGzipExtensions.join('|') +
111 | ')$'
112 | ),
113 | threshold: 10240,
114 | minRatio: 0.8
115 | })
116 | )
117 | }
118 |
119 | if (config.build.bundleAnalyzerReport) {
120 | const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin
121 | webpackConfig.plugins.push(new BundleAnalyzerPlugin())
122 | }
123 |
124 | module.exports = webpackConfig
125 |
--------------------------------------------------------------------------------
/config/dev.env.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 | const merge = require('webpack-merge')
3 | const prodEnv = require('./prod.env')
4 |
5 | module.exports = merge(prodEnv, {
6 | NODE_ENV: '"development"'
7 | })
8 |
--------------------------------------------------------------------------------
/config/index.js:
--------------------------------------------------------------------------------
1 |
2 | 'use strict'
3 | // Template version: 1.1.3
4 | // see http://vuejs-templates.github.io/webpack for documentation.
5 |
6 | const path = require('path')
7 |
8 | module.exports = {
9 | build: {
10 | env: require('./prod.env'),
11 | index: path.resolve(__dirname, '../dist/index.html'),
12 | assetsRoot: path.resolve(__dirname, '../dist'),
13 | assetsSubDirectory: 'static',
14 | assetsPublicPath: '/',
15 | productionSourceMap: true,
16 | // Gzip off by default as many popular static hosts such as
17 | // Surge or Netlify already gzip all static assets for you.
18 | // Before setting to `true`, make sure to:
19 | // npm install --save-dev compression-webpack-plugin
20 | productionGzip: false,
21 | productionGzipExtensions: ['js', 'css'],
22 | // Run the build command with an extra argument to
23 | // View the bundle analyzer report after build finishes:
24 | // `npm run build --report`
25 | // Set to `true` or `false` to always turn it on or off
26 | bundleAnalyzerReport: process.env.npm_config_report
27 | },
28 | dev: {
29 | env: require('./dev.env'),
30 | port: process.env.PORT || 8080,
31 | autoOpenBrowser: true,
32 | assetsSubDirectory: 'static',
33 | assetsPublicPath: '/',
34 | proxyTable: {},
35 | // CSS Sourcemaps off by default because relative paths are "buggy"
36 | // with this option, according to the CSS-Loader README
37 | // (https://github.com/webpack/css-loader#sourcemaps)
38 | // In our experience, they generally work as expected,
39 | // just be aware of this issue when enabling this option.
40 | cssSourceMap: false
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/config/prod.env.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 | require('dotenv').config()
3 |
4 | module.exports = {
5 | NODE_ENV: '"production"',
6 | FIREBASE_API_KEY: JSON.stringify(process.env.FIREBASE_API_KEY),
7 | FIREBASE_AUTH_DOMAIN: JSON.stringify(process.env.FIREBASE_AUTH_DOMAIN),
8 | FIREBASE_DATABASE_URL: JSON.stringify(process.env.FIREBASE_DATABASE_URL),
9 | FIREBASE_PROJECT_ID: JSON.stringify(process.env.FIREBASE_PROJECT_ID),
10 | FIREBASE_STORAGE_BUCKET: JSON.stringify(process.env.FIREBASE_STORAGE_BUCKET),
11 | FIREBASE_MESSAGING_ID: JSON.stringify(process.env.FIREBASE_MESSAGING_ID)
12 | }
13 |
--------------------------------------------------------------------------------
/database.rules.json:
--------------------------------------------------------------------------------
1 | {
2 | "rules": {
3 | ".read": true,
4 | ".write": true
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/firebase.json:
--------------------------------------------------------------------------------
1 | {
2 | "database": {
3 | "rules": "database.rules.json"
4 | }
5 | }
6 |
--------------------------------------------------------------------------------
/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | vueschool-forum
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "vueschool-forum",
3 | "version": "1.0.0",
4 | "description": "Awesome full blown mega madness forum",
5 | "author": "Alex Kyriakidis ",
6 | "private": true,
7 | "scripts": {
8 | "dev": "node build/dev-server.js",
9 | "start": "npm run dev",
10 | "build": "node build/build.js",
11 | "lint": "eslint --ext .js,.vue src",
12 | "db:seed": "firebase database:set / src/data.json -y"
13 | },
14 | "dependencies": {
15 | "dotenv": "^5.0.1",
16 | "firebase": "^4.12.1",
17 | "moment": "^2.19.3",
18 | "nprogress": "^0.2.0",
19 | "vue": "^2.5.2",
20 | "vue-router": "^3.0.1",
21 | "vuelidate": "^0.7.4",
22 | "vuex": "^3.0.1"
23 | },
24 | "devDependencies": {
25 | "autoprefixer": "^7.1.2",
26 | "babel-core": "^6.22.1",
27 | "babel-eslint": "^7.1.1",
28 | "babel-loader": "^7.1.1",
29 | "babel-plugin-transform-runtime": "^6.22.0",
30 | "babel-preset-env": "^1.3.2",
31 | "babel-preset-stage-2": "^6.22.0",
32 | "babel-register": "^6.22.0",
33 | "chalk": "^2.0.1",
34 | "connect-history-api-fallback": "^1.3.0",
35 | "copy-webpack-plugin": "^4.0.1",
36 | "css-loader": "^0.28.0",
37 | "eslint": "^3.19.0",
38 | "eslint-config-standard": "^10.2.1",
39 | "eslint-friendly-formatter": "^3.0.0",
40 | "eslint-loader": "^1.7.1",
41 | "eslint-plugin-html": "^3.0.0",
42 | "eslint-plugin-import": "^2.7.0",
43 | "eslint-plugin-node": "^5.2.0",
44 | "eslint-plugin-promise": "^3.4.0",
45 | "eslint-plugin-standard": "^3.0.1",
46 | "eventsource-polyfill": "^0.9.6",
47 | "express": "^4.14.1",
48 | "extract-text-webpack-plugin": "^3.0.0",
49 | "file-loader": "^1.1.4",
50 | "friendly-errors-webpack-plugin": "^1.6.1",
51 | "html-webpack-plugin": "^2.30.1",
52 | "http-proxy-middleware": "^0.17.3",
53 | "opn": "^5.1.0",
54 | "optimize-css-assets-webpack-plugin": "^3.2.0",
55 | "ora": "^1.2.0",
56 | "portfinder": "^1.0.13",
57 | "rimraf": "^2.6.0",
58 | "semver": "^5.3.0",
59 | "shelljs": "^0.7.6",
60 | "url-loader": "^0.5.8",
61 | "vue-loader": "^13.3.0",
62 | "vue-style-loader": "^3.0.1",
63 | "vue-template-compiler": "^2.5.2",
64 | "webpack": "^3.6.0",
65 | "webpack-bundle-analyzer": "^2.9.0",
66 | "webpack-dev-middleware": "^1.12.0",
67 | "webpack-hot-middleware": "^2.18.2",
68 | "webpack-merge": "^4.1.0"
69 | },
70 | "engines": {
71 | "node": ">= 4.0.0",
72 | "npm": ">= 3.0.0"
73 | },
74 | "browserslist": [
75 | "> 1%",
76 | "last 2 versions",
77 | "not ie <= 8"
78 | ]
79 | }
80 |
--------------------------------------------------------------------------------
/src/App.vue:
--------------------------------------------------------------------------------
1 |
2 |
9 |
10 |
11 |
49 |
50 |
58 |
--------------------------------------------------------------------------------
/src/assets/css/style.css:
--------------------------------------------------------------------------------
1 | @charset "UTF-8";
2 |
3 | body {
4 | background-color: #F6F8FF;
5 | min-height: 100vh;
6 | }
7 |
8 | *, *:after, *:before {
9 | box-sizing: border-box;
10 | }
11 |
12 | @media (min-width: 1024px) {
13 | html {
14 | font-size: 16px;
15 | }
16 | }
17 |
18 | @media (min-width: 240px) and (max-width: 1023px) {
19 | html {
20 | font-size: 14px;
21 | }
22 | }
23 |
24 | body {
25 | line-height: 1.5;
26 | margin: 0;
27 | padding: 0;
28 | }
29 |
30 | img {
31 | height: auto;
32 | max-width: 100%;
33 | }
34 |
35 | figure {
36 | margin: 0 0 20px 0;
37 | padding: 0;
38 | text-align: center;
39 | }
40 |
41 | figcaption {
42 | display: block;
43 | text-align: center;
44 | font-size: .8rem;
45 | }
46 |
47 | .list-title {
48 | background-color: #263959;
49 | border-bottom-left-radius: 20px;
50 | color: #f5f8fe;
51 | font-weight: 100;
52 | display: flex;
53 | width: 100%;
54 | justify-content: flex-start;
55 | position: relative;
56 | padding: 10px 20px;
57 | margin: 0;
58 | }
59 |
60 | .list-title a {
61 | color: white;
62 | }
63 |
64 | .list-title a:hover {
65 | color: #89c6af;
66 | }
67 |
68 | .img-round, .avatar, .avatar-xsmall, .avatar-small, .avatar-medium, .avatar-large, .avatar-xlarge {
69 | border-radius: 50%;
70 | max-width: 100%;
71 | }
72 |
73 | .forum-list {
74 | padding: 0;
75 | background: white;
76 | margin: 20px 0;
77 | }
78 |
79 | .forum-list .forum-listing {
80 | display: flex;
81 | flex-wrap: wrap;
82 | justify-content: space-between;
83 | align-items: center;
84 | padding: 20px 10px 20px 30px;
85 | }
86 |
87 | .forum-list .forum-listing:nth-child(odd) {
88 | background: rgba(73, 89, 96, 0.06);
89 | border-bottom-left-radius: 20px;
90 | }
91 |
92 | .forum-list .forum-listing:last-child {
93 | border-bottom-left-radius: 0;
94 | }
95 |
96 | .forum-list .forum-listing .forum-details {
97 | flex-basis: 52%;
98 | }
99 |
100 | @media (min-width: 240px) and (max-width: 720px) {
101 | .forum-list .forum-listing .forum-details {
102 | flex-basis: 100%;
103 | }
104 | }
105 |
106 | .forum-list .forum-listing .forum-details ul.subforums {
107 | padding-left: 5px;
108 | display: block;
109 | }
110 |
111 | .forum-list .forum-listing .forum-details ul.subforums::before {
112 | content: '⌙';
113 | margin-right: 5px;
114 | }
115 |
116 | .forum-list .forum-listing .forum-details ul.subforums.subforums li {
117 | display: inline;
118 | }
119 |
120 | .forum-list .forum-listing .forum-details ul.subforums.subforums li:not(:last-of-type)::after {
121 | content: '\f111';
122 | font-family: 'FontAwesome';
123 | font-size: 4px;
124 | position: relative;
125 | top: -3px;
126 | left: 2px;
127 | padding: 0 3px;
128 | color: #878787;
129 | }
130 |
131 | .forum-list .forum-listing .threads-count {
132 | flex-basis: 12%;
133 | text-align: center;
134 | }
135 |
136 | .forum-list .forum-listing .threads-count .count {
137 | font-weight: 100;
138 | display: block;
139 | }
140 |
141 | .forum-list .forum-listing .last-thread {
142 | flex-basis: 32%;
143 | display: flex;
144 | justify-content: flex-start;
145 | align-items: center;
146 | }
147 |
148 | .forum-list .forum-listing .last-thread .avatar {
149 | margin-right: 10px;
150 | }
151 |
152 | .forum-header {
153 | display: flex;
154 | justify-content: space-between;
155 | align-items: flex-end;
156 | }
157 |
158 | .forum-stats ul {
159 | font-size: 0px;
160 | display: flex;
161 | justify-content: center;
162 | margin-bottom: 50px;
163 | }
164 |
165 | .forum-stats ul li {
166 | display: flex;
167 | font-weight: 100;
168 | margin: 0 20px;
169 | align-items: center;
170 | }
171 |
172 | .forum-stats ul li .fa {
173 | margin-right: 5px;
174 | }
175 |
176 | .forum-stats ul li .fa-comments-o {
177 | font-size: 26px;
178 | }
179 |
180 | .thread-list {
181 | padding: 0;
182 | background-color: white;
183 | }
184 |
185 | .thread-list .thread {
186 | display: flex;
187 | justify-content: space-between;
188 | align-items: center;
189 | padding: 5px 0 5px 20px;
190 | min-height: 45px;
191 | }
192 |
193 | .thread-list .thread:nth-child(odd) {
194 | background: rgba(73, 89, 96, 0.06);
195 | border-bottom-left-radius: 20px;
196 | }
197 |
198 | .thread-list .thread:last-child {
199 | border-bottom-left-radius: 0;
200 | }
201 |
202 | .thread-list .thread .replies-count {
203 | flex-basis: 35%;
204 | }
205 |
206 | .thread-list .thread .activity {
207 | flex-basis: 35%;
208 | display: flex;
209 | justify-content: flex-start;
210 | align-items: center;
211 | }
212 |
213 | .thread-list .thread .activity .avatar-medium {
214 | margin-right: 10px;
215 | }
216 |
217 | .thread-header {
218 | display: flex;
219 | justify-content: space-between;
220 | align-items: center;
221 | }
222 |
223 | .reactions {
224 | display: flex;
225 | justify-content: flex-end;
226 | flex: 100%;
227 | position: relative;
228 | }
229 |
230 | .reactions button {
231 | display: flex;
232 | align-items: center;
233 | padding: 5px 8px;
234 | margin-left: 2px;
235 | color: #545454;
236 | border-radius: 5px;
237 | }
238 |
239 | .reactions button:hover {
240 | background: rgba(115, 192, 151, 0.25) !important;
241 | color: #545454 !important;
242 | }
243 |
244 | .reactions button.active-reaction {
245 | background: rgba(115, 192, 151, 0.12);
246 | }
247 |
248 | .reactions button.active-reaction:hover {
249 | background: white !important;
250 | }
251 |
252 | .reactions button .emoji {
253 | margin-right: 3px;
254 | font-size: 18px;
255 | }
256 |
257 | .reactions button.add-reaction .emoji {
258 | margin-left: 3px;
259 | margin-right: 0px;
260 | }
261 |
262 | .reactions ul {
263 | position: absolute;
264 | display: flex;
265 | justify-content: flex-end;
266 | top: -45px;
267 | background-color: white !important;
268 | }
269 |
270 | .reactions ul li {
271 | font-size: 28px;
272 | display: flex;
273 | align-items: center;
274 | padding: 0px 5px;
275 | opacity: 0.7;
276 | }
277 |
278 | .reactions ul li:hover {
279 | opacity: 1;
280 | border-radius: 5px;
281 | cursor: pointer;
282 | }
283 |
284 | .pagination {
285 | display: flex;
286 | align-items: center;
287 | justify-content: center;
288 | margin-top: 40px;
289 | margin-bottom: 40px;
290 | color: #838486;
291 | }
292 |
293 | .pagination button {
294 | background: #95cbb7;
295 | display: flex;
296 | align-items: center;
297 | justify-content: center;
298 | margin: 0 15px;
299 | padding: 0px;
300 | height: 35px;
301 | width: 35px;
302 | font-size: 22px;
303 | }
304 |
305 | .pagination button:hover {
306 | background: #57AD8D;
307 | }
308 |
309 | .pagination button:disabled {
310 | cursor: not-allowed;
311 | }
312 |
313 | .pagination button:disabled:hover {
314 | background: #95cbb7;
315 | }
316 |
317 | .pagination button:disabled:active {
318 | animation: shake 0.82s cubic-bezier(0.36, 0.07, 0.19, 0.97) both;
319 | transform: translate3d(0, 0, 0);
320 | backface-visibility: hidden;
321 | perspective: 1000px;
322 | }
323 |
324 | @keyframes shake {
325 | 10%, 90% {
326 | transform: translate3d(-1px, 0, 0);
327 | }
328 | 20%, 80% {
329 | transform: translate3d(2px, 0, 0);
330 | }
331 | 30%, 50%, 70% {
332 | transform: translate3d(-4px, 0, 0);
333 | }
334 | 40%, 60% {
335 | transform: translate3d(4px, 0, 0);
336 | }
337 | }
338 |
339 | .post-list {
340 | margin-top: 20px;
341 | }
342 |
343 | .post {
344 | display: flex;
345 | flex-wrap: wrap;
346 | justify-content: space-between;
347 | background-color: white;
348 | padding: 20px 10px;
349 | padding-bottom: 7px;
350 | box-shadow: 2px 2px 1px rgba(136, 136, 136, 0.09);
351 | margin-bottom: 20px;
352 | }
353 |
354 | @media (max-width: 820px) {
355 | .post {
356 | padding: 0;
357 | }
358 | }
359 |
360 | .post .user-info {
361 | display: flex;
362 | flex-direction: column;
363 | align-items: center;
364 | justify-content: flex-start;
365 | text-align: center;
366 | flex: 1 1 15%;
367 | margin-right: 5px;
368 | }
369 |
370 | .post .user-info > * {
371 | margin-bottom: 10px;
372 | }
373 |
374 | @media (max-width: 820px) {
375 | .post .user-info {
376 | order: -2;
377 | flex-direction: row;
378 | justify-content: flex-start;
379 | background: rgba(73, 89, 96, 0.06);
380 | margin-right: 0;
381 | padding: 5px;
382 | padding-left: 10px;
383 | }
384 |
385 | .post .user-info .avatar-large {
386 | height: 35px;
387 | width: 35px;
388 | margin-right: 5px;
389 | order: 1;
390 | }
391 |
392 | .post .user-info .user-name {
393 | order: 2;
394 | }
395 |
396 | .post .user-info > * {
397 | margin-right: 5px;
398 | margin-bottom: 0;
399 | }
400 | }
401 |
402 | .post .post-date {
403 | flex-basis: 100%;
404 | font-size: 14px;
405 | text-align: right;
406 | margin-bottom: 5px;
407 | padding-right: 7px;
408 | }
409 |
410 | @media (max-width: 820px) {
411 | .post .post-date {
412 | order: -1;
413 | flex-basis: 40%;
414 | background: rgba(73, 89, 96, 0.06);
415 | padding-right: 10px;
416 | padding-top: 16px;
417 | margin-bottom: 0px;
418 | }
419 | }
420 |
421 | @media (max-width: 720px) {
422 | .post {
423 | padding: 0px;
424 | }
425 | }
426 |
427 | .post-content {
428 | display: flex;
429 | flex: 1 0 83%;
430 | padding-left: 15px;
431 | padding-right: 10px;
432 | font-size: 16px;
433 | text-align: justify;
434 | line-height: 1.5;
435 | word-break: break-word;
436 | }
437 |
438 | .post-content h1, .post-content h2, .post-content h3 {
439 | margin-bottom: 0;
440 | }
441 |
442 | .post-content p {
443 | margin-bottom: 20px;
444 | }
445 |
446 | .post-content pre {
447 | display: grid;
448 | overflow: auto;
449 | word-wrap: break-word;
450 | border-radius: 3px;
451 | padding: 10px;
452 | }
453 |
454 | .post-content blockquote {
455 | margin: 25px 0px;
456 | }
457 |
458 | .post-content blockquote.big {
459 | display: flex;
460 | position: relative;
461 | }
462 |
463 | .post-content blockquote.big::before {
464 | position: absolute;
465 | top: -25px;
466 | left: -25px;
467 | font-size: 42px;
468 | font-family: FontAwesome;
469 | content: "\f10e";
470 | color: #263959;
471 | }
472 |
473 | @media (max-width: 820px) {
474 | .post-content blockquote.big::before {
475 | top: -15px;
476 | left: -18px;
477 | font-size: 32px;
478 | }
479 | }
480 |
481 | .post-content blockquote.big .quote {
482 | padding-left: 20px;
483 | padding-right: 15px;
484 | flex-basis: 95%;
485 | font-weight: 100;
486 | font-style: italic;
487 | font-size: 17px;
488 | }
489 |
490 | .post-content blockquote.big .author {
491 | display: flex;
492 | flex-direction: column;
493 | align-items: center;
494 | justify-content: flex-start;
495 | text-align: center;
496 | }
497 |
498 | .post-content blockquote.big .author img {
499 | flex: 1;
500 | flex-basis: 100%;
501 | margin-top: 10px;
502 | width: 80px;
503 | height: 80px;
504 | }
505 |
506 | .post-content blockquote.small {
507 | / / display: flex;
508 | position: relative;
509 | flex-direction: column;
510 | border: 2px solid rgba(152, 152, 152, 0.15);
511 | border-bottom-left-radius: 5px;
512 | border-bottom-right-radius: 5px;
513 | }
514 |
515 | .post-content blockquote.small::before {
516 | position: absolute;
517 | top: -20px;
518 | left: -20px;
519 | font-size: 42px;
520 | font-family: FontAwesome;
521 | content: "\f10e";
522 | color: #263959;
523 | }
524 |
525 | @media (max-width: 820px) {
526 | .post-content blockquote.small::before {
527 | top: -18px;
528 | left: -15px;
529 | font-size: 32px;
530 | }
531 | }
532 |
533 | .post-content blockquote.small .author {
534 | display: flex;
535 | flex-basis: 100%;
536 | padding: 3px 10px 3px 28px;
537 | background-color: rgba(152, 152, 152, 0.15);
538 | justify-content: center;
539 | align-items: center;
540 | }
541 |
542 | .post-content blockquote.small .author .time {
543 | margin-left: 10px;
544 | }
545 |
546 | .post-content blockquote.small .author .fa {
547 | margin-left: auto;
548 | font-size: 20px;
549 | }
550 |
551 | .post-content blockquote.small .author .fa:hover {
552 | cursor: pointer;
553 | }
554 |
555 | .post-content blockquote.small .quote {
556 | display: flex;
557 | flex-basis: 100%;
558 | flex-direction: column;
559 | padding: 10px;
560 | font-weight: 100;
561 | font-style: italic;
562 | font-size: 17px;
563 | }
564 |
565 | .post-content blockquote.simple {
566 | position: relative;
567 | padding: 0px 10px 0px 20px;
568 | font-weight: 100;
569 | font-style: italic;
570 | font-size: 17px;
571 | letter-spacing: .15px;
572 | }
573 |
574 | .post-content blockquote.simple::before {
575 | position: absolute;
576 | top: -25px;
577 | left: -25px;
578 | font-size: 42px;
579 | font-family: FontAwesome;
580 | content: "\f10e";
581 | color: #263959;
582 | }
583 |
584 | @media (max-width: 820px) {
585 | .post-content blockquote.simple::before {
586 | top: -15px;
587 | left: -18px;
588 | font-size: 32px;
589 | }
590 | }
591 |
592 | .post-content blockquote.simple .author {
593 | display: block;
594 | margin-top: 10px;
595 | font-weight: normal;
596 | }
597 |
598 | .post-content blockquote.simple .author .time {
599 | margin-left: 10px;
600 | }
601 |
602 | .post-listing-editor {
603 | flex: 1 1 83%;
604 | }
605 |
606 | .profile-card {
607 | padding: 10px 20px 20px 20px;
608 | margin-bottom: 10px;
609 | background: white;
610 | box-shadow: 2px 2px 1px rgba(136, 136, 136, 0.09);
611 | align-self: self-end;
612 | }
613 |
614 | @media (min-width: 820px) {
615 | .profile-card {
616 | margin-right: 20px;
617 | }
618 | }
619 |
620 | .profile-card .title {
621 | word-break: break-all;
622 | }
623 |
624 | .profile-card .stats {
625 | display: flex;
626 | margin: 20px 0px;
627 | }
628 |
629 | .profile-card .stats span {
630 | flex-basis: 50%;
631 | }
632 |
633 | .profile-card .user-website {
634 | display: flex;
635 | justify-content: center;
636 | align-items: baseline;
637 | }
638 |
639 | .profile-header {
640 | display: flex;
641 | align-items: baseline;
642 | justify-content: space-between;
643 | padding: 0 0px;
644 | }
645 |
646 | @media (max-width: 720px) {
647 | .profile-header {
648 | flex-wrap: wrap;
649 | }
650 | }
651 |
652 | @media (min-width: 1024px) {
653 | .activity-list {
654 | padding: 0px 10px;
655 | }
656 | }
657 |
658 | .activity-list .activity {
659 | background-color: white;
660 | padding: 15px 10px;
661 | margin-bottom: 20px;
662 | box-shadow: 2px 2px 1px rgba(136, 136, 136, 0.09);
663 | }
664 |
665 | @media (max-width: 720px) {
666 | .activity-list .activity {
667 | padding: 10px 15px;
668 | }
669 |
670 | .activity-list .activity .post-content {
671 | padding-left: 0;
672 | }
673 | }
674 |
675 | .activity-list .activity .activity-header {
676 | margin: 0;
677 | flex: 1;
678 | display: flex;
679 | flex-wrap: wrap;
680 | align-items: flex-start;
681 | justify-content: flex-end;
682 | }
683 |
684 | .activity-list .activity .activity-header img {
685 | margin-top: 5px;
686 | margin-right: 10px;
687 | }
688 |
689 | .activity-list .activity .activity-header .title {
690 | flex-basis: 93%;
691 | margin: 0;
692 | padding: 0;
693 | }
694 |
695 | @media (max-width: 720px) {
696 | .activity-list .activity .activity-header .title {
697 | flex-basis: 100%;
698 | }
699 | }
700 |
701 | .activity-list .activity .activity-header .title span {
702 | display: block;
703 | font-weight: 100;
704 | }
705 |
706 | .activity-list .activity div.post-content {
707 | display: block;
708 | padding-right: 10px;
709 | margin: 12px 0px;
710 | word-break: break-word;
711 | }
712 |
713 | .activity-list .activity div.post-content p {
714 | margin-bottom: 12px;
715 | }
716 |
717 | .activity-list .activity .thread-details {
718 | text-align: right;
719 | }
720 |
721 | .activity-list .activity .thread-details span:not(:last-of-type) {
722 | margin-right: 20px;
723 | }
724 |
725 | textarea#user_bio {
726 | resize: vertical;
727 | }
728 |
729 | span.offline::before {
730 | font-family: FontAwesome;
731 | content: "\f1db";
732 | font-size: 14px;
733 | margin-right: 5px;
734 | }
735 |
736 | span.online {
737 | color: #57AD8D;
738 | }
739 |
740 | span.online::before {
741 | font-family: FontAwesome;
742 | content: "\f2be";
743 | font-size: 14px;
744 | margin-right: 5px;
745 | }
746 |
747 | .header {
748 | display: flex;
749 | justify-content: space-between;
750 | align-items: center;
751 | background: #263959;
752 | height: 80px;
753 | padding: 0 20px;
754 | }
755 |
756 | @media (min-width: 240px) and (max-width: 720px) {
757 | .header {
758 | justify-content: space-between;
759 | align-items: center;
760 | padding: 0 10px;
761 | height: 60px;
762 | }
763 | }
764 |
765 | .logo {
766 | float: left;
767 | padding-top: 5px;
768 | }
769 |
770 | @media (min-width: 240px) and (max-width: 720px) {
771 | .logo {
772 | padding-top: 10px;
773 | }
774 | }
775 |
776 | .svg-logo {
777 | height: 62px;
778 | width: 56px;
779 | }
780 |
781 | @media (min-width: 240px) and (max-width: 720px) {
782 | .svg-logo {
783 | height: 45px;
784 | width: 40px;
785 | }
786 | }
787 |
788 | @media (min-width: 240px) and (max-width: 400px) {
789 | .svg-logo {
790 | height: 40px;
791 | width: 35px;
792 | }
793 | }
794 |
795 | .wrap-right {
796 | float: right;
797 | padding: 10px 10px;
798 | }
799 |
800 | @media (min-width: 240px) and (max-width: 720px) {
801 | .wrap-right {
802 | padding: 16px 0;
803 | }
804 | }
805 |
806 | .text-faded, .forum-stats ul li, .thread-list .thread .created_at, .post-content blockquote.big .author span.time, .post-content blockquote.small .author .time, .post-content blockquote.simple .author, .activity-list .activity .activity-header .title span, .activity-list .activity .thread-details, span.offline {
807 | color: rgba(84, 84, 84, 0.7);
808 | }
809 |
810 | h1 {
811 | font-size: 32px;
812 | }
813 |
814 | @media (min-width: 240px) and (max-width: 720px) {
815 | h1 {
816 | font-size: 24px;
817 | }
818 | }
819 |
820 | h2 {
821 | font-size: 28px;
822 | }
823 |
824 | @media (min-width: 240px) and (max-width: 720px) {
825 | h2 {
826 | font-size: 20px;
827 | }
828 | }
829 |
830 | .text-lead, .forum-list .forum-listing .threads-count .count, .profile-card .stats span, .modal-container .modal .modal-header .title {
831 | font-size: 26px;
832 | line-height: 1.5;
833 | font-weight: 300;
834 | }
835 |
836 | @media (min-width: 240px) and (max-width: 720px) {
837 | .text-lead, .forum-list .forum-listing .threads-count .count, .profile-card .stats span, .modal-container .modal .modal-header .title {
838 | font-size: 22px;
839 | }
840 | }
841 |
842 | .text, p, .text-xsmall, .thread-list .thread .created_at, .pagination, .post-content blockquote.small .author .time, .post-content blockquote.simple .author .time, .activity-list .activity .thread-details, span.offline, span.online, .btn-xsmall, .btn-brown, ul.breadcrumbs li:not(:last-of-type)::after, .text-small, .forum-list .forum-listing .forum-details ul.subforums, .post-content blockquote.big .author, .post-content blockquote.small .author a, .post-content blockquote.simple .author, .activity-list .activity .activity-header .title span, .btn-small, ul.breadcrumbs li, .text-large, .list-title, .forum-stats ul li, .profile-card .user-website, .activity-list .activity .activity-header .title, .btn-large, .text-xlarge, .btn-xlarge, .btn, .btn-blue, .btn-blue-outlined, .btn-brown-outlined, .btn-green, .btn-green-outlined, .btn-red, .btn-red-outlined, .btn-ghost {
843 | font-size: 16px;
844 | line-height: 1.5;
845 | }
846 |
847 | @media (min-width: 240px) and (max-width: 720px) {
848 | .text, p, .text-xsmall, .thread-list .thread .created_at, .pagination, .post-content blockquote.small .author .time, .post-content blockquote.simple .author .time, .activity-list .activity .thread-details, span.offline, span.online, .btn-xsmall, .btn-brown, ul.breadcrumbs li:not(:last-of-type)::after, .text-small, .forum-list .forum-listing .forum-details ul.subforums, .post-content blockquote.big .author, .post-content blockquote.small .author a, .post-content blockquote.simple .author, .activity-list .activity .activity-header .title span, .btn-small, ul.breadcrumbs li, .text-large, .list-title, .forum-stats ul li, .profile-card .user-website, .activity-list .activity .activity-header .title, .btn-large, .text-xlarge, .btn-xlarge, .btn, .btn-blue, .btn-blue-outlined, .btn-brown-outlined, .btn-green, .btn-green-outlined, .btn-red, .btn-red-outlined, .btn-ghost {
849 | font-size: 15px;
850 | }
851 | }
852 |
853 | .text-xsmall, .thread-list .thread .created_at, .pagination, .post-content blockquote.small .author .time, .post-content blockquote.simple .author .time, .activity-list .activity .thread-details, span.offline, span.online, .btn-xsmall, .btn-brown, ul.breadcrumbs li:not(:last-of-type)::after {
854 | font-size: 13px;
855 | }
856 |
857 | @media (min-width: 240px) and (max-width: 720px) {
858 | .text-xsmall, .thread-list .thread .created_at, .pagination, .post-content blockquote.small .author .time, .post-content blockquote.simple .author .time, .activity-list .activity .thread-details, span.offline, span.online, .btn-xsmall, .btn-brown, ul.breadcrumbs li:not(:last-of-type)::after {
859 | font-size: 12px;
860 | }
861 | }
862 |
863 | .text-small, .forum-list .forum-listing .forum-details ul.subforums, .post-content blockquote.big .author, .post-content blockquote.small .author a, .post-content blockquote.simple .author, .activity-list .activity .activity-header .title span, .btn-small, ul.breadcrumbs li {
864 | font-size: 15px;
865 | }
866 |
867 | @media (min-width: 240px) and (max-width: 720px) {
868 | .text-small, .forum-list .forum-listing .forum-details ul.subforums, .post-content blockquote.big .author, .post-content blockquote.small .author a, .post-content blockquote.simple .author, .activity-list .activity .activity-header .title span, .btn-small, ul.breadcrumbs li {
869 | font-size: 14px;
870 | }
871 | }
872 |
873 | .text-large, .list-title, .forum-stats ul li, .profile-card .user-website, .activity-list .activity .activity-header .title, .btn-large {
874 | font-size: 18px;
875 | }
876 |
877 | @media (min-width: 240px) and (max-width: 720px) {
878 | .text-large, .list-title, .forum-stats ul li, .profile-card .user-website, .activity-list .activity .activity-header .title, .btn-large {
879 | font-size: 17px;
880 | }
881 | }
882 |
883 | .text-xlarge, .btn-xlarge {
884 | font-size: 22px;
885 | }
886 |
887 | @media (min-width: 240px) and (max-width: 720px) {
888 | .text-xlarge, .btn-xlarge {
889 | font-size: 20px;
890 | }
891 | }
892 |
893 | .text-bold, .activity-list .activity .activity-header .title {
894 | font-weight: bold;
895 | }
896 |
897 | .text-italic {
898 | font-style: italic;
899 | }
900 |
901 | .text-underline {
902 | text-decoration: underline;
903 | }
904 |
905 | .text-line-through {
906 | text-decoration: line-through;
907 | }
908 |
909 | .text-center, .profile-card .stats span, .profile-card .user-website {
910 | text-align: center;
911 | }
912 |
913 | .text-left, .activity-list .activity .activity-header .title {
914 | text-align: left;
915 | }
916 |
917 | .text-right {
918 | text-align: right;
919 | }
920 |
921 | .text-justify {
922 | text-align: justify;
923 | }
924 |
925 | ul {
926 | margin: 0;
927 | padding: 0;
928 | }
929 |
930 | .navbar {
931 | width: 100%;
932 | display:flex;
933 | flex-direction: row-reverse;
934 | justify-content: space-between;
935 | }
936 |
937 | .navbar ul {
938 | display: flex;
939 | align-items: center;
940 | justify-content: flex-start;
941 | height: 100%;
942 | }
943 |
944 | .navbar-item, .navbar-mobile-item {
945 | display: inline-block;
946 | border-right: 1px solid #3c4d6a;
947 | vertical-align: middle;
948 | }
949 |
950 | ul .navbar-item:last-child, ul .navbar-mobile-item:last-child {
951 | border-right: none;
952 | }
953 |
954 | .navbar-item a, .navbar-mobile-item a {
955 | color: white;
956 | padding: 10px 20px;
957 | text-decoration: none;
958 | transition: all .6s ease;
959 | font-size: 18px;
960 | }
961 |
962 | @media (min-width: 240px) and (max-width: 720px) {
963 | .navbar-item a, .navbar-mobile-item a {
964 | padding: 10px 0px;
965 | }
966 | }
967 |
968 | .navbar-item a:hover, .navbar-mobile-item a:hover {
969 | color: #57AD8D;
970 | }
971 |
972 | .navbar-item a:active, .navbar-mobile-item a:active {
973 | color: #57AD8D;
974 | }
975 |
976 | @media (min-width: 240px) and (max-width: 720px) {
977 | .navbar-item, .navbar-mobile-item {
978 | display: block;
979 | border: none;
980 | margin: 20px 0;
981 | }
982 | }
983 |
984 | @media (min-width: 240px) and (max-width: 720px) {
985 | .navbar {
986 | display: none;
987 | position: absolute;
988 | z-index: 10;
989 | padding: 10px 10px 10px;
990 | background: #263959;
991 | width: 100%;
992 | left: 0;
993 | top: 60px;
994 | }
995 | }
996 |
997 | @media (min-width: 240px) and (max-width: 720px) {
998 | .navbar-open {
999 | display: flex;
1000 | transition: all 0.6s ease;
1001 | border-bottom-right-radius: 5px;
1002 | border-bottom-left-radius: 5px;
1003 | }
1004 |
1005 | .navbar-open .navbar-item, .navbar-open .navbar-mobile-item {
1006 | margin: 6px 0;
1007 | }
1008 |
1009 | .navbar-open ul {
1010 | flex: 1;
1011 | justify-content: flex-start;
1012 | align-items: flex-start;
1013 | flex-direction: column;
1014 | padding-left: 20px;
1015 | }
1016 | }
1017 |
1018 | .signs .navbar-item, .signs .navbar-mobile-item {
1019 | border-right: none;
1020 | }
1021 |
1022 | .a-active {
1023 | color: #57AD8D;
1024 | }
1025 |
1026 | .icon-profile {
1027 | width: 10px;
1028 | height: 8px;
1029 | }
1030 |
1031 | .navbar-user {
1032 | margin-left: auto;
1033 | }
1034 |
1035 | .navbar-user a {
1036 | display: flex;
1037 | align-items: center;
1038 | color: white;
1039 | }
1040 |
1041 | .navbar-user a:hover .icon-profile {
1042 | transition: all .4s ease;
1043 | transform: rotate(-180deg);
1044 | }
1045 |
1046 | .navbar-user img {
1047 | margin-right: 10px;
1048 | }
1049 |
1050 | .btn-hamburger {
1051 | cursor: pointer;
1052 | height: 30px;
1053 | width: 30px;
1054 | float: right;
1055 | position: relative;
1056 | margin-left: 20px;
1057 | display: none;
1058 | }
1059 |
1060 | .btn-hamburger .top {
1061 | top: 7px;
1062 | }
1063 |
1064 | .btn-hamburger .middle {
1065 | top: 16px;
1066 | }
1067 |
1068 | .btn-hamburger .bottom {
1069 | top: 26px;
1070 | }
1071 |
1072 | @media (min-width: 240px) and (max-width: 720px) {
1073 | .btn-hamburger {
1074 | display: block;
1075 | }
1076 | }
1077 |
1078 | .bar {
1079 | width: 30px;
1080 | height: 4px;
1081 | background: white;
1082 | position: absolute;
1083 | border-radius: 10px;
1084 | transition: all 0.5s;
1085 | }
1086 |
1087 | .btn-hamburger-active .top {
1088 | top: 16px;
1089 | }
1090 |
1091 | .btn-hamburger-active .middle {
1092 | opacity: 0;
1093 | overflow: hidden;
1094 | }
1095 |
1096 | .btn-hamburger-active .bottom {
1097 | top: 16px;
1098 | }
1099 |
1100 | header > a.logo {
1101 | width: 50px;
1102 | }
1103 |
1104 | @media (min-width: 240px) and (max-width: 720px) {
1105 | header > a.logo {
1106 | width: 35px;
1107 | }
1108 | }
1109 |
1110 | .title {
1111 | font-size: 38px;
1112 | text-align: center;
1113 | }
1114 |
1115 | @media (min-width: 1360px) {
1116 | .title {
1117 | font-size: 46px;
1118 | }
1119 | }
1120 |
1121 | @media (min-width: 600px) and (max-width: 1023px) {
1122 | .title {
1123 | font-size: 32px;
1124 | }
1125 | }
1126 |
1127 | @media (min-width: 720px) and (max-width: 820px) {
1128 | .title {
1129 | font-size: 30px;
1130 | }
1131 | }
1132 |
1133 | @media (min-width: 240px) and (max-width: 720px) {
1134 | .title {
1135 | font-size: 30px;
1136 | }
1137 | }
1138 |
1139 | .title-white {
1140 | color: white;
1141 | }
1142 |
1143 | .title-banner {
1144 | color: white;
1145 | text-transform: uppercase;
1146 | }
1147 |
1148 | .subtitle {
1149 | font-size: 26px;
1150 | }
1151 |
1152 | @media (min-width: 600px) and (max-width: 1023px) {
1153 | .subtitle {
1154 | font-size: 22px;
1155 | }
1156 | }
1157 |
1158 | @media (min-width: 240px) and (max-width: 720px) {
1159 | .subtitle {
1160 | font-size: 20px;
1161 | }
1162 | }
1163 |
1164 | @media (min-width: 240px) and (max-width: 400px) {
1165 | .subtitle {
1166 | font-size: 18px;
1167 | }
1168 | }
1169 |
1170 | #user-dropdown {
1171 | position: absolute;
1172 | top: 50px;
1173 | right: 20px;
1174 | z-index: 6;
1175 | display: none;
1176 | }
1177 |
1178 | @media (min-width: 240px) and (max-width: 720px) {
1179 | #user-dropdown {
1180 | position: relative;
1181 | width: 100%;
1182 | right: 0;
1183 | z-index: 10;
1184 | top: 98px;
1185 | }
1186 | }
1187 |
1188 | #user-dropdown.active-drop {
1189 | display: block;
1190 | }
1191 |
1192 | .active-drop {
1193 | display: block;
1194 | }
1195 |
1196 | .dropdown-menu, #user-dropdown > .dropdown-menu {
1197 | display: block;
1198 | background: white;
1199 | padding: 20px;
1200 | position: relative;
1201 | }
1202 |
1203 | .dropdown-menu-item, #user-dropdown > .dropdown-menu > .dropdown-menu-item {
1204 | margin-bottom: 5px;
1205 | }
1206 |
1207 | .dropdown-menu-item a, #user-dropdown > .dropdown-menu > .dropdown-menu-item a {
1208 | display: block;
1209 | color: #57AD8D;
1210 | font-size: 16px;
1211 | transition: all ease 0.6s;
1212 | }
1213 |
1214 | .dropdown-menu-item a:hover, #user-dropdown > .dropdown-menu > .dropdown-menu-item a:hover {
1215 | color: #41826a;
1216 | }
1217 |
1218 | .triangle-drop {
1219 | border-bottom: solid 8px white;
1220 | border-left: solid 8px transparent;
1221 | border-right: solid 8px transparent;
1222 | display: inline-block;
1223 | margin: 0;
1224 | position: relative;
1225 | left: 70%;
1226 | vertical-align: middle;
1227 | bottom: -8px;
1228 | }
1229 |
1230 | @media (min-width: 240px) and (max-width: 720px) {
1231 | .triangle-drop {
1232 | left: 5%;
1233 | }
1234 | }
1235 |
1236 | #user-dropdown a {
1237 | color: #57AD8D;
1238 | text-decoration: none;
1239 | transition: all .6s ease;
1240 | }
1241 |
1242 | #user-dropdown a:hover {
1243 | color: #41826a;
1244 | cursor: pointer;
1245 | }
1246 |
1247 | #user-dropdown ul {
1248 | display: block;
1249 | }
1250 |
1251 | .mentionsList {
1252 | position: absolute;
1253 | width: 160px;
1254 | z-index: 2;
1255 | background-color: #263959;
1256 | box-shadow: 2px 2px 1px rgba(136, 136, 136, 0.09);
1257 | color: white;
1258 | }
1259 |
1260 | .mentionsList li {
1261 | padding: 6px;
1262 | display: flex;
1263 | align-items: center;
1264 | }
1265 |
1266 | .mentionsList li img {
1267 | margin-right: 2px;
1268 | }
1269 |
1270 | .mentionsList li:hover {
1271 | cursor: pointer;
1272 | background-color: #57AD8D;
1273 | }
1274 |
1275 | .mentionsList li:not(:last-of-type) {
1276 | margin-bottom: 3px;
1277 | }
1278 |
1279 | .mentionsList::before {
1280 | border-bottom: solid 8px #263959;
1281 | border-left: solid 8px transparent;
1282 | border-right: solid 8px transparent;
1283 | content: "";
1284 | display: inline-block;
1285 | left: 52px;
1286 | position: absolute;
1287 | top: -8px;
1288 | }
1289 |
1290 | .mentionsList .arrow-up {
1291 | width: 0;
1292 | height: 0;
1293 | border-left: 5px solid transparent;
1294 | border-right: 5px solid transparent;
1295 | border-bottom: 5px solid black;
1296 | }
1297 |
1298 | input {
1299 | box-shadow: none;
1300 | }
1301 |
1302 | form {
1303 | margin: 0;
1304 | }
1305 |
1306 | .form-input {
1307 | border: 1px solid #ddd;
1308 | border-radius: 5px;
1309 | box-sizing: border-box;
1310 | font: inherit;
1311 | padding: 5px 10px;
1312 | transition: all 0.3s ease;
1313 | width: 100%;
1314 | color: #505050;
1315 | background-color: #fdfdfd;
1316 | min-height: 43px;
1317 | }
1318 |
1319 | .form-input:disabled {
1320 | cursor: no-drop;
1321 | background: #F5F8FE;
1322 | color: #bbbbbb;
1323 | }
1324 |
1325 | .form-input:disabled::placeholder {
1326 | color: #bbbbbb;
1327 | }
1328 |
1329 | .form-input::placeholder {
1330 | font-size: inherit;
1331 | font-weight: 300;
1332 | color: #878787;
1333 | }
1334 |
1335 | .form-input:focus {
1336 | outline: none;
1337 | border: 1px solid #c7c7c7;
1338 | color: #434343;
1339 | background-color: white;
1340 | }
1341 |
1342 | .form-input:invalid {
1343 | border-color: #C82543;
1344 | }
1345 |
1346 | .form-input:invalid ~ .form-error {
1347 | display: block;
1348 | }
1349 |
1350 | @media (min-width: 240px) and (max-width: 400px) {
1351 | .form-input {
1352 | padding-left: 10px;
1353 | height: 50px;
1354 | }
1355 | }
1356 |
1357 | textarea.form-input {
1358 | padding-top: 7px;
1359 | padding-right: 2px;
1360 | padding-bottom: 0px;
1361 | min-height: 110px;
1362 | }
1363 |
1364 | .input-error {
1365 | border-color: #C82543;
1366 | }
1367 |
1368 | .input-error ~ .form-error {
1369 | display: block;
1370 | }
1371 |
1372 | .form-error {
1373 | background: #f4d3d9;
1374 | color: #C82543;
1375 | font-size: 0.8em;
1376 | float: left;
1377 | border-radius: 100px;
1378 | padding: 6px 20px;
1379 | margin-top: 10px;
1380 | }
1381 |
1382 | @media (min-width: 240px) and (max-width: 400px) {
1383 | .form-error {
1384 | width: 100%;
1385 | }
1386 | }
1387 |
1388 | .form-group {
1389 | margin-bottom: 12px;
1390 | width: 100%;
1391 | display: inline-block;
1392 | }
1393 |
1394 | .form-label, .form-group > label {
1395 | margin-bottom: 5px;
1396 | display: inline-block;
1397 | color: #767676;
1398 | }
1399 |
1400 | .form-label-password, .form-group > label-password {
1401 | margin-bottom: 0px;
1402 | }
1403 |
1404 | @media (min-width: 240px) and (max-width: 720px) {
1405 | .form-btn {
1406 | width: 100%;
1407 | }
1408 | }
1409 |
1410 | input[type="submit"],
1411 | button {
1412 | -webkit-appearance: none;
1413 | font-size: 18px;
1414 | cursor: pointer;
1415 | }
1416 |
1417 | button {
1418 | -webkit-appearance: none;
1419 | }
1420 |
1421 | button a {
1422 | color: white;
1423 | }
1424 |
1425 | .form-2cols {
1426 | display: flex;
1427 | flex-wrap: wrap;
1428 | }
1429 |
1430 | .form-2cols .form-group {
1431 | flex-basis: 47%;
1432 | }
1433 |
1434 | .form-2cols .form-group:nth-child(odd) {
1435 | margin-right: 10px;
1436 | }
1437 |
1438 | @media (min-width: 240px) and (max-width: 720px) {
1439 | .form-2cols .form-group {
1440 | flex-basis: 100%;
1441 | margin-right: 0;
1442 | }
1443 | }
1444 |
1445 | button {
1446 | border: none;
1447 | background: transparent;
1448 | appearance: none;
1449 | }
1450 |
1451 | .btn, .btn-blue, .btn-blue-outlined, .btn-brown, .btn-brown-outlined, .btn-green, .btn-green-outlined, .btn-red, .btn-red-outlined, .btn-ghost {
1452 | padding: 15px 30px;
1453 | border-radius: 5px;
1454 | border: none;
1455 | display: inline-block;
1456 | outline: 0;
1457 | transition: all 0.4s ease;
1458 | }
1459 |
1460 | @media (min-width: 240px) and (max-width: 720px) {
1461 | .btn, .btn-blue, .btn-blue-outlined, .btn-brown, .btn-brown-outlined, .btn-green, .btn-green-outlined, .btn-red, .btn-red-outlined, .btn-ghost {
1462 | padding: 10px 20px;
1463 | }
1464 | }
1465 |
1466 | .btn:disabled, .btn-blue:disabled, .btn-blue-outlined:disabled, .btn-brown:disabled, .btn-brown-outlined:disabled, .btn-green:disabled, .btn-green-outlined:disabled, .btn-red:disabled, .btn-red-outlined:disabled, .btn-ghost:disabled, .btn-disabled {
1467 | cursor: default;
1468 | }
1469 |
1470 | .btn:disabled:hover, .btn-blue:disabled:hover, .btn-blue-outlined:disabled:hover, .btn-brown:disabled:hover, .btn-brown-outlined:disabled:hover, .btn-green:disabled:hover, .btn-green-outlined:disabled:hover, .btn-red:disabled:hover, .btn-red-outlined:disabled:hover, .btn-ghost:disabled:hover, .btn-disabled:hover {
1471 | cursor: default;
1472 | color: white;
1473 | }
1474 |
1475 | .btn-block {
1476 | width: 100%;
1477 | }
1478 |
1479 | .btn-xsmall {
1480 | padding: 6px 15px;
1481 | }
1482 |
1483 | .btn-small {
1484 | padding: 10px 20px;
1485 | }
1486 |
1487 | .btn-large {
1488 | padding: 20px 40px;
1489 | }
1490 |
1491 | .btn-xlarge {
1492 | padding: 20px 60px;
1493 | }
1494 |
1495 | .btn-circle {
1496 | height: 60px;
1497 | width: 60px;
1498 | background: #C82543;
1499 | border-radius: 50%;
1500 | transition: all ease 0.4s;
1501 | padding: 0px;
1502 | font-size: 36px;
1503 | display: flex;
1504 | justify-content: center;
1505 | align-content: center;
1506 | color: white;
1507 | }
1508 |
1509 | .btn-circle:hover {
1510 | background: #b4213c;
1511 | }
1512 |
1513 | .btn-circle:hover .icon-arrow-up {
1514 | transition: all ease 0.4s;
1515 | transform: translateY(-2px);
1516 | }
1517 |
1518 | .btn-circle:hover .icon-arrow {
1519 | transition: all ease 0.4s;
1520 | transform: translateX(2px);
1521 | }
1522 |
1523 | .btn-circle:hover .icon-arrow-left {
1524 | transition: all ease 0.4s;
1525 | transform: translateX(-2px) rotate(180deg);
1526 | }
1527 |
1528 | .btn-circle-default {
1529 | background: #878787;
1530 | }
1531 |
1532 | .btn-circle-default:hover {
1533 | background: #4c4c4c;
1534 | }
1535 |
1536 | @media (min-width: 240px) and (max-width: 720px) {
1537 | .btn-circle {
1538 | height: 45px;
1539 | width: 45px;
1540 | padding-top: 14px;
1541 | }
1542 | }
1543 |
1544 | .btn-blue {
1545 | color: white;
1546 | background: #263959;
1547 | }
1548 |
1549 | .btn-blue:hover:not(:disabled):not(.btn-disabled) {
1550 | color: white;
1551 | background: #1d2b43;
1552 | }
1553 |
1554 | .btn-blue-outlined {
1555 | color: #263959;
1556 | box-shadow: inset 0px 0px 0px 1.6px #263959;
1557 | }
1558 |
1559 | .btn-blue-outlined:hover {
1560 | color: white;
1561 | background: #263959;
1562 | }
1563 |
1564 | .btn-brown {
1565 | color: white;
1566 | background: #bf9268;
1567 | }
1568 |
1569 | .btn-brown:hover:not(:disabled):not(.btn-disabled) {
1570 | color: white;
1571 | background: #8f6e4e;
1572 | }
1573 |
1574 | .btn-brown-outlined {
1575 | color: #bf9268;
1576 | box-shadow: inset 0px 0px 0px 1.6px #bf9268;
1577 | }
1578 |
1579 | .btn-brown-outlined:hover {
1580 | color: white;
1581 | background: #bf9268;
1582 | }
1583 |
1584 | .btn-green {
1585 | color: white;
1586 | background: #57AD8D;
1587 | }
1588 |
1589 | .btn-green:hover:not(:disabled):not(.btn-disabled) {
1590 | color: white;
1591 | background: #4e9c7f;
1592 | }
1593 |
1594 | .btn-green-outlined {
1595 | color: #57AD8D;
1596 | box-shadow: inset 0px 0px 0px 1.6px #57AD8D;
1597 | }
1598 |
1599 | .btn-green-outlined:hover {
1600 | color: white;
1601 | background: #57AD8D;
1602 | }
1603 |
1604 | .btn-red {
1605 | color: white;
1606 | background: #C82543;
1607 | }
1608 |
1609 | .btn-red:hover:not(:disabled):not(.btn-disabled) {
1610 | color: white;
1611 | background: #b4213c;
1612 | }
1613 |
1614 | .btn-red-outlined {
1615 | color: #C82543;
1616 | box-shadow: inset 0px 0px 0px 1.6px #C82543;
1617 | }
1618 |
1619 | .btn-red-outlined:hover {
1620 | color: white;
1621 | background: #C82543;
1622 | }
1623 |
1624 | .btn-green {
1625 | color: white;
1626 | background: #57AD8D;
1627 | }
1628 |
1629 | .btn-green:hover:not(:disabled):not(.btn-disabled) {
1630 | color: white;
1631 | background: #4e9c7f;
1632 | }
1633 |
1634 | .btn-red {
1635 | color: white;
1636 | background: #C82543;
1637 | }
1638 |
1639 | .btn-red:hover:not(:disabled):not(.btn-disabled) {
1640 | color: white;
1641 | background: #b4213c;
1642 | }
1643 |
1644 | .btn-ghost {
1645 | flex-grow: 0;
1646 | }
1647 |
1648 | .btn-ghost:hover:not(:disabled):not(.btn-disabled) {
1649 | color: white;
1650 | background-color: rgba(152, 152, 152, 0.31);
1651 | }
1652 |
1653 | .btn-up {
1654 | height: 40px;
1655 | width: 40px;
1656 | padding-top: 10px;
1657 | text-align: center;
1658 | }
1659 |
1660 | .btn-input {
1661 | height: 52px;
1662 | line-height: 48px;
1663 | position: absolute;
1664 | border: none;
1665 | color: #fff;
1666 | cursor: pointer;
1667 | font-family: 'Open sans', sans-serif;
1668 | font-size: 18px;
1669 | padding: 0 25px;
1670 | right: 4px;
1671 | top: 4px;
1672 | transition: background 0.15s ease;
1673 | z-index: 10;
1674 | }
1675 |
1676 | @media (min-width: 240px) and (max-width: 720px) {
1677 | .btn-input {
1678 | position: static;
1679 | margin-top: 10px;
1680 | width: 100%;
1681 | }
1682 | }
1683 |
1684 | .btn-social {
1685 | margin-right: 6px;
1686 | }
1687 |
1688 | .btn-social svg {
1689 | height: 40px;
1690 | width: 40px;
1691 | transition: all ease 0.6s;
1692 | transform: rotate(0);
1693 | }
1694 |
1695 | .btn-social svg:hover {
1696 | transform: rotate(-40deg);
1697 | }
1698 |
1699 | .icon-arrow-up {
1700 | width: 12px;
1701 | height: 18px;
1702 | transform: translate(0, 0);
1703 | }
1704 |
1705 | .icon-arrow {
1706 | height: 21px;
1707 | transform: translate(0, 0);
1708 | width: 28px;
1709 | }
1710 |
1711 | @media (min-width: 240px) and (max-width: 720px) {
1712 | .icon-arrow {
1713 | height: 14px;
1714 | width: 21px;
1715 | }
1716 | }
1717 |
1718 | .icon-arrow-left {
1719 | width: 28px;
1720 | height: 21px;
1721 | transform: translate(0, 0);
1722 | transform: rotate(180deg);
1723 | }
1724 |
1725 | @media (min-width: 240px) and (max-width: 720px) {
1726 | .icon-arrow-left {
1727 | height: 14px;
1728 | width: 21px;
1729 | }
1730 | }
1731 |
1732 | .link {
1733 | color: #57AD8D;
1734 | text-decoration: underline;
1735 | transition: all ease 0.4s;
1736 | }
1737 |
1738 | .link:hover {
1739 | color: #468a71;
1740 | }
1741 |
1742 | button {
1743 | outline: 0;
1744 | }
1745 |
1746 | .form-actions, .btn-group {
1747 | display: flex;
1748 | justify-content: flex-end;
1749 | flex-basis: 100%;
1750 | margin-top: 10px;
1751 | margin-bottom: 10px;
1752 | }
1753 |
1754 | .form-actions > *:not(:last-child), .btn-group > *:not(:last-child) {
1755 | margin-right: 10px;
1756 | }
1757 |
1758 | @media (min-width: 240px) and (max-width: 720px) {
1759 | .form-actions, .btn-group {
1760 | flex-wrap: wrap;
1761 | }
1762 |
1763 | .form-actions > *:not(.btn-ghost), .btn-group > *:not(.btn-ghost) {
1764 | flex: 1 1;
1765 | margin-bottom: 5px;
1766 | }
1767 | }
1768 |
1769 | .space-between {
1770 | justify-content: space-between;
1771 | }
1772 |
1773 | .alert {
1774 | width: 100%;
1775 | padding: 10px 20px;
1776 | color: white;
1777 | opacity: 0.8;
1778 | position: relative;
1779 | z-index: 1;
1780 | display: flex;
1781 | align-items: center;
1782 | justify-content: space-between;
1783 | font-size: 0.9rem;
1784 | margin-bottom: 5px;
1785 | }
1786 |
1787 | .alert-error {
1788 | background: #C82543;
1789 | }
1790 |
1791 | .alert-success {
1792 | background: #57AD8D;
1793 | }
1794 |
1795 | .alert-info {
1796 | background: #51617a;
1797 | }
1798 |
1799 | @media (min-width: 240px) and (max-width: 720px) {
1800 | .alert {
1801 | padding: 10px;
1802 | }
1803 | }
1804 |
1805 | .close {
1806 | color: white;
1807 | background: transparent;
1808 | border: none;
1809 | }
1810 |
1811 | @media (min-width: 240px) and (max-width: 720px) {
1812 | .close {
1813 | align-self: flex-start;
1814 | margin-top: 4px;
1815 | }
1816 | }
1817 |
1818 | .close-icon {
1819 | stroke: #fff;
1820 | height: 12px;
1821 | width: 12px;
1822 | }
1823 |
1824 | .avatar {
1825 | width: 50px;
1826 | max-width: 50px;
1827 | height: 50px;
1828 | max-height: 50px;
1829 | }
1830 |
1831 | .avatar-xsmall {
1832 | width: 25px;
1833 | max-width: 25px;
1834 | height: 25px;
1835 | max-height: 25px;
1836 | }
1837 |
1838 | .avatar-small {
1839 | width: 35px;
1840 | max-width: 35px;
1841 | height: 35px;
1842 | max-height: 35px;
1843 | }
1844 |
1845 | .avatar-medium {
1846 | width: 35px;
1847 | max-width: 35px;
1848 | height: 35px;
1849 | max-height: 35px;
1850 | }
1851 |
1852 | .avatar-large {
1853 | width: 95px;
1854 | max-width: 95px;
1855 | height: 95px;
1856 | max-height: 95px;
1857 | }
1858 |
1859 | .avatar-xlarge {
1860 | width: 200px;
1861 | max-width: 200px;
1862 | height: 200px;
1863 | max-height: 200px;
1864 | }
1865 |
1866 | .card {
1867 | background: white;
1868 | margin-top: 20px;
1869 | }
1870 |
1871 | @media (min-width: 240px) and (max-width: 720px) {
1872 | .card {
1873 | margin-bottom: 20px;
1874 | }
1875 | }
1876 |
1877 | .card-form {
1878 | padding: 40px 60px;
1879 | position: relative;
1880 | z-index: 1;
1881 | margin-top: 40px;
1882 | }
1883 |
1884 | @media (min-width: 1360px) {
1885 | .card-form {
1886 | padding: 60px;
1887 | }
1888 | }
1889 |
1890 | @media (min-width: 600px) and (max-width: 1023px) {
1891 | .card-form {
1892 | padding: 40px;
1893 | }
1894 | }
1895 |
1896 | @media (min-width: 240px) and (max-width: 720px) {
1897 | .card-form {
1898 | padding: 40px 20px;
1899 | margin-top: 10px;
1900 | }
1901 | }
1902 |
1903 | .striped {
1904 | background: white;
1905 | box-shadow: 1px 1px 1px #f1f1f1;
1906 | }
1907 |
1908 | .striped li {
1909 | padding: 10px 5px 10px 12px;
1910 | box-shadow: 0 1px rgba(73, 89, 96, 0.06);
1911 | }
1912 |
1913 | .striped li:nth-child(even) {
1914 | background: rgba(73, 89, 96, 0.06);
1915 | }
1916 |
1917 | .sidebar {
1918 | display: none;
1919 | }
1920 |
1921 | @media (min-width: 1024px) {
1922 | .sidebar {
1923 | display: flex;
1924 | flex-basis: 29%;
1925 | margin: 0 0.5%;
1926 | margin-top: 118px;
1927 | flex-direction: column;
1928 | }
1929 |
1930 | .sidebar .widget {
1931 | background: white;
1932 | margin-bottom: 10px;
1933 | }
1934 | }
1935 |
1936 | .sidebar ul > li {
1937 | display: flex;
1938 | flex-wrap: wrap;
1939 | align-items: center;
1940 | }
1941 |
1942 | .sidebar ul > li > span {
1943 | flex-basis: 85%;
1944 | }
1945 |
1946 | .sidebar .unanswered-threads-list {
1947 | margin-top: 10px;
1948 | }
1949 |
1950 | ul.breadcrumbs {
1951 | list-style: none;
1952 | overflow: auto;
1953 | font-size: 0;
1954 | }
1955 |
1956 | ul.breadcrumbs li {
1957 | display: inline-block;
1958 | padding: 5px 0px;
1959 | font-weight: 100;
1960 | }
1961 |
1962 | ul.breadcrumbs li:not(:last-of-type)::after {
1963 | content: '\f105';
1964 | font-family: FontAwesome;
1965 | margin: 0px 4px;
1966 | opacity: 0.6;
1967 | }
1968 |
1969 | ul.breadcrumbs li a {
1970 | color: #57AD8D;
1971 | text-decoration: none;
1972 | opacity: 0.7;
1973 | }
1974 |
1975 | ul.breadcrumbs li a:hover {
1976 | opacity: 1;
1977 | }
1978 |
1979 | #moderation {
1980 | display: flex;
1981 | }
1982 |
1983 | #moderation.justify-right {
1984 | margin-right: 20px;
1985 | }
1986 |
1987 | @media (min-width: 240px) and (max-width: 720px) {
1988 | #moderation.justify-right {
1989 | margin: 0;
1990 | }
1991 | }
1992 |
1993 | #moderation ul.toolbar {
1994 | z-index: 99;
1995 | display: flex;
1996 | flex-wrap: wrap;
1997 | position: fixed;
1998 | bottom: 20px;
1999 | box-shadow: 0px 0px 300px #ADADAD;
2000 | padding: 0 5px;
2001 | border-radius: 5px;
2002 | background-color: #313131;
2003 | }
2004 |
2005 | #moderation ul.toolbar li {
2006 | margin: 10px 0;
2007 | }
2008 |
2009 | #moderation ul.toolbar li.close-toolbar {
2010 | display: flex;
2011 | justify-content: center;
2012 | align-items: center;
2013 | padding: 0;
2014 | margin: 0;
2015 | }
2016 |
2017 | #moderation ul.toolbar li.close-toolbar .fa {
2018 | font-size: 30px;
2019 | }
2020 |
2021 | #moderation ul.toolbar li.close-toolbar a {
2022 | padding: 0 10px;
2023 | }
2024 |
2025 | #moderation ul.toolbar.open-toolbar {
2026 | display: none;
2027 | }
2028 |
2029 | #moderation ul.toolbar:not(:last-of-type) {
2030 | border-right: 1px solid rgba(255, 255, 255, 0.3);
2031 | }
2032 |
2033 | #moderation ul.toolbar a {
2034 | display: inline-block;
2035 | color: white;
2036 | line-height: 1.5;
2037 | padding: 5px 20px 5px 10px;
2038 | }
2039 |
2040 | #moderation ul.toolbar a:hover .fa {
2041 | opacity: 1;
2042 | }
2043 |
2044 | #moderation ul.toolbar a .fa {
2045 | opacity: 0.5;
2046 | margin: 0px 8px;
2047 | }
2048 |
2049 | #moderation ul.toolbar a:focus {
2050 | outline: none;
2051 | }
2052 |
2053 | @media (min-width: 240px) and (max-width: 720px) {
2054 | #moderation ul.toolbar {
2055 | position: fixed;
2056 | bottom: 0;
2057 | margin: 0;
2058 | padding: 0;
2059 | width: 100%;
2060 | border-bottom-left-radius: 0;
2061 | border-bottom-right-radius: 0;
2062 | }
2063 |
2064 | #moderation ul.toolbar li {
2065 | flex-basis: 100%;
2066 | margin: 0;
2067 | text-align: center;
2068 | border-right: none !important;
2069 | }
2070 |
2071 | #moderation ul.toolbar li a {
2072 | display: block;
2073 | border: none;
2074 | font-size: 18px;
2075 | padding: 7px 0;
2076 | }
2077 |
2078 | #moderation ul.toolbar li.close-toolbar .fa::before {
2079 | content: '\f107';
2080 | font-family: FontAwesome;
2081 | }
2082 | }
2083 |
2084 | #moderation ul.toolbar-collapsed {
2085 | opacity: 0.6;
2086 | }
2087 |
2088 | #moderation ul.toolbar-collapsed:hover {
2089 | opacity: 1;
2090 | }
2091 |
2092 | #moderation ul.toolbar-collapsed li, #moderation ul.toolbar-collapsed li.close-toolbar {
2093 | display: none;
2094 | }
2095 |
2096 | #moderation ul.toolbar-collapsed li.open-toolbar {
2097 | display: inline-block;
2098 | border: none;
2099 | }
2100 |
2101 | #moderation ul.toolbar-collapsed li.open-toolbar a {
2102 | padding: 0 10px 0 0;
2103 | }
2104 |
2105 | @media (min-width: 240px) and (max-width: 720px) {
2106 | #moderation ul.toolbar-collapsed {
2107 | border-radius: 0;
2108 | }
2109 |
2110 | #moderation ul.toolbar-collapsed li.open-toolbar {
2111 | display: inline-block;
2112 | border: none;
2113 | }
2114 |
2115 | #moderation ul.toolbar-collapsed li.open-toolbar a {
2116 | padding: 0;
2117 | font-size: 0;
2118 | line-height: 0.95;
2119 | }
2120 |
2121 | #moderation ul.toolbar-collapsed li.open-toolbar a .fa {
2122 | margin: 0;
2123 | }
2124 |
2125 | #moderation ul.toolbar-collapsed li.open-toolbar a::before {
2126 | content: '\f106';
2127 | font-family: FontAwesome;
2128 | font-size: 30px;
2129 | }
2130 | }
2131 |
2132 | @media (min-width: 240px) and (max-width: 720px) {
2133 | body {
2134 | padding-bottom: 20px;
2135 | }
2136 | }
2137 |
2138 | .modal-container {
2139 | position: fixed;
2140 | top: 0;
2141 | left: 0;
2142 | z-index: 100;
2143 | display: flex;
2144 | justify-content: center;
2145 | background: rgba(0, 0, 0, 0.4);
2146 | height: 100vh;
2147 | width: 100vw;
2148 | }
2149 |
2150 | .modal-container .modal {
2151 | display: flex;
2152 | flex-wrap: wrap;
2153 | z-index: 200;
2154 | position: fixed;
2155 | top: 10vh;
2156 | width: 50vw;
2157 | max-width: 550px;
2158 | min-height: 25vh;
2159 | background: #F5F8FE;
2160 | background: #fcfdff;
2161 | background-color: white;
2162 | border-radius: 8px;
2163 | }
2164 |
2165 | @media screen and (min-width: 240px) and (max-width: 900px) {
2166 | .modal-container .modal {
2167 | top: 5vh;
2168 | width: 95vw;
2169 | min-height: 40vh;
2170 | }
2171 | }
2172 |
2173 | .modal-container .modal hr {
2174 | margin: 5px;
2175 | }
2176 |
2177 | .modal-container .modal .btn-group {
2178 | margin: 0;
2179 | padding: 0;
2180 | }
2181 |
2182 | .modal-container .modal .modal-header, .modal-container .modal .modal-footer {
2183 | padding: 15px;
2184 | flex-basis: 100%;
2185 | }
2186 |
2187 | .modal-container .modal .modal-header {
2188 | border-bottom: 3px solid rgba(73, 89, 96, 0.06);
2189 | }
2190 |
2191 | .modal-container .modal .modal-header .title {
2192 | font-size: 32px;
2193 | }
2194 |
2195 | .modal-container .modal .modal-content {
2196 | padding: 10px 30px;
2197 | min-height: 200px;
2198 | }
2199 |
2200 | .modal-container .modal .modal-footer {
2201 | background: rgba(73, 89, 96, 0.06);
2202 | border-bottom-left-radius: 8px;
2203 | border-bottom-right-radius: 8px;
2204 | }
2205 |
2206 | a.close {
2207 | display: flex;
2208 | position: absolute;
2209 | right: 10px;
2210 | top: 10px;
2211 | color: #263959;
2212 | font-size: 22px;
2213 | opacity: .7;
2214 | }
2215 |
2216 | a.close:hover {
2217 | opacity: 1;
2218 | color: #263959;
2219 | }
2220 |
2221 | body {
2222 | font-family: 'Open Sans', sans-serif;
2223 | color: #545454;
2224 | font-size: 16px;
2225 | line-height: 1.5;
2226 | overflow-x: hidden;
2227 | box-sizing: border-box;
2228 | }
2229 |
2230 | @media (min-width: 240px) and (max-width: 720px) {
2231 | body {
2232 | font-size: 15px;
2233 | }
2234 | }
2235 |
2236 | body a {
2237 | color: #57AD8D;
2238 | text-decoration: none;
2239 | transition: all .6s ease;
2240 | }
2241 |
2242 | body a:hover {
2243 | color: #41826a;
2244 | cursor: pointer;
2245 | }
2246 |
2247 | h1, h2, h3, h4, h5 {
2248 | font-weight: 700;
2249 | margin-bottom: 10px;
2250 | margin-top: 0;
2251 | margin-left: 0;
2252 | margin-right: 0;
2253 | }
2254 |
2255 | ul {
2256 | padding: 0;
2257 | }
2258 |
2259 | li {
2260 | list-style: none;
2261 | }
2262 |
2263 | li a {
2264 | text-decoration: none;
2265 | }
2266 |
2267 | p {
2268 | margin: 0;
2269 | }
2270 |
2271 | figure {
2272 | margin: 0;
2273 | }
2274 |
2275 | .flex-column {
2276 | display: flex;
2277 | margin: 0 auto;
2278 | max-width: 1000px;
2279 | flex-direction: column;
2280 | }
2281 |
2282 | @media (min-width: 1360px) {
2283 | .flex-column {
2284 | max-width: 1300px;
2285 | }
2286 | }
2287 |
2288 | .flex-grid {
2289 | display: flex;
2290 | margin: 0 auto;
2291 | max-width: 1200px;
2292 | flex-grow: 1;
2293 | flex-wrap: wrap;
2294 | }
2295 |
2296 | @media (min-width: 1360px) {
2297 | .flex-grid {
2298 | max-width: 1300px;
2299 | }
2300 | }
2301 |
2302 | .flex-reverse {
2303 | flex-direction: row-reverse;
2304 | }
2305 |
2306 | .align-center {
2307 | align-items: center;
2308 | }
2309 |
2310 | .col {
2311 | margin: 0 1%;
2312 | }
2313 |
2314 | @media (min-width: 240px) and (max-width: 720px) {
2315 | .col {
2316 | margin: 0 4%;
2317 | }
2318 | }
2319 |
2320 | .col-70 {
2321 | flex-basis: 70%;
2322 | margin: 0 auto;
2323 | }
2324 |
2325 | @media (min-width: 1360px) {
2326 | .col-70 {
2327 | flex-basis: 70%;
2328 | }
2329 | }
2330 |
2331 | @media (min-width: 600px) and (max-width: 1023px) {
2332 | .col-70 {
2333 | flex-basis: 80%;
2334 | }
2335 | }
2336 |
2337 | @media (min-width: 240px) and (max-width: 720px) {
2338 | .col-70 {
2339 | flex-basis: 96%;
2340 | margin: 0 4%;
2341 | }
2342 | }
2343 |
2344 | .col-2 {
2345 | box-sizing: border-box;
2346 | flex-basis: 48%;
2347 | }
2348 |
2349 | @media (min-width: 240px) and (max-width: 720px) {
2350 | .col-2 {
2351 | flex-basis: 96%;
2352 | }
2353 | }
2354 |
2355 | .col-3 {
2356 | flex-basis: 31%;
2357 | }
2358 |
2359 | @media (min-width: 600px) and (max-width: 1023px) {
2360 | .col-3 {
2361 | flex-basis: 96%;
2362 | margin: 4% 2%;
2363 | }
2364 | }
2365 |
2366 | @media (min-width: 240px) and (max-width: 720px) {
2367 | .col-3 {
2368 | flex-basis: 96%;
2369 | margin: 4% 4%;
2370 | }
2371 | }
2372 |
2373 | .col-4 {
2374 | flex-basis: 23%;
2375 | }
2376 |
2377 | @media (min-width: 240px) and (max-width: 720px) {
2378 | .col-4 {
2379 | flex-basis: 46%;
2380 | }
2381 | }
2382 |
2383 | .col-5 {
2384 | flex-basis: 18%;
2385 | }
2386 |
2387 | @media (min-width: 240px) and (max-width: 720px) {
2388 | .col-5 {
2389 | flex-basis: 46%;
2390 | }
2391 | }
2392 |
2393 | .col-7 {
2394 | flex-basis: 68%;
2395 | }
2396 |
2397 | @media (min-width: 600px) and (max-width: 1023px) {
2398 | .col-7 {
2399 | flex-basis: 96%;
2400 | margin: 4% 2%;
2401 | }
2402 | }
2403 |
2404 | @media (min-width: 240px) and (max-width: 720px) {
2405 | .col-7 {
2406 | flex-basis: 96%;
2407 | margin: 4% 4%;
2408 | }
2409 | }
2410 |
2411 | .container {
2412 | display: flex;
2413 | max-width: 1200px;
2414 | margin: 0 auto;
2415 | align-items: center;
2416 | justify-content: center;
2417 | flex-wrap: wrap;
2418 | }
2419 |
2420 | @media (min-width: 240px) and (max-width: 720px) {
2421 | .container {
2422 | width: 96%;
2423 | }
2424 | }
2425 |
2426 | .col-full {
2427 | flex-basis: 98%;
2428 | margin: 0 1%;
2429 | }
2430 |
2431 | @media (max-width: 820px) {
2432 | .col-full {
2433 | flex-basis: 98%;
2434 | margin: 0 1%;
2435 | }
2436 | }
2437 |
2438 | .col-large {
2439 | flex-basis: 72%;
2440 | margin: 0 1%;
2441 | }
2442 |
2443 | @media (max-width: 1024px) {
2444 | .col-large {
2445 | flex-basis: 98%;
2446 | margin: 0 1%;
2447 | }
2448 | }
2449 |
2450 | .col-small {
2451 | flex-basis: 24%;
2452 | margin: 0 1%;
2453 | }
2454 |
2455 | @media (min-width: 240px) and (max-width: 720px) {
2456 | .col-small {
2457 | flex-basis: 98%;
2458 | margin: 0 1%;
2459 | }
2460 | }
2461 |
2462 | .align-center {
2463 | align-items: center;
2464 | justify-content: center;
2465 | }
2466 |
2467 | .justify-center {
2468 | justify-content: center;
2469 | }
2470 |
2471 | .justify-left {
2472 | justify-content: flex-start;
2473 | }
2474 |
2475 | .justify-right {
2476 | justify-content: flex-end;
2477 | }
2478 |
2479 | .justify-space-between {
2480 | justify-content: space-between;
2481 | }
2482 |
2483 | @media (max-width: 820px) {
2484 | .desktop-only, .forum-list .forum-listing .threads-count, .forum-list .forum-listing .last-thread, .forum-stats, .thread-list .thread .activity, .navbar-user {
2485 | display: none;
2486 | }
2487 | }
2488 |
2489 | @media (min-width: 820px) {
2490 | .mobile-only, .navbar-mobile-item {
2491 | display: none;
2492 | }
2493 | }
2494 |
2495 | @media (max-width: 720px) {
2496 | .hide-mobile {
2497 | display: none;
2498 | }
2499 | }
2500 |
2501 | @media (min-width: 720px) and (max-width: 820px) {
2502 | .hide-tablet {
2503 | display: none;
2504 | }
2505 | }
2506 |
2507 | @media (max-width: 720px) {
2508 | .hide-desktop {
2509 | display: none;
2510 | }
2511 | }
2512 |
2513 | section {
2514 | margin-top: 20px;
2515 | }
2516 |
2517 | .push-top {
2518 | margin-top: 20px;
2519 | }
2520 |
2521 | .no-margin {
2522 | margin: 0 !important;
2523 | }
2524 |
2525 | .link-white {
2526 | color: white;
2527 | }
2528 |
2529 | .link-unstyled, ul.breadcrumbs li a {
2530 | color: inherit;
2531 | }
2532 |
2533 | .faded, .btn:disabled, .btn-blue:disabled, .btn-blue-outlined:disabled, .btn-brown:disabled, .btn-brown-outlined:disabled, .btn-green:disabled, .btn-green-outlined:disabled, .btn-red:disabled, .btn-red-outlined:disabled, .btn-ghost:disabled, .btn-disabled, a > img:hover {
2534 | opacity: 0.8;
2535 | }
2536 |
2537 | a > img {
2538 | transition: all .6s ease;
2539 | }
2540 |
2541 | hr {
2542 | border: 0;
2543 | height: 1px;
2544 | background: #333;
2545 | background-image: linear-gradient(to right, #F7F9FE, #D1D3D7, #F7F9FE);
2546 | margin-bottom: 20px;
2547 | }
2548 |
2549 | .fa-btn {
2550 | padding-right: 3px;
2551 | }
2552 |
2553 | #app {
2554 | background: #F5F8FE;
2555 | min-height: 100vh;
2556 | }
--------------------------------------------------------------------------------
/src/assets/img/arrow-profile.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/assets/img/vueschool-logo.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/assets/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/vueschool/vue-master-class/4a2febe07c9873c96c84d5108f03768096af3084/src/assets/logo.png
--------------------------------------------------------------------------------
/src/components/AppDate.vue:
--------------------------------------------------------------------------------
1 |
2 | {{timestamp | diffForHumans}}
3 |
4 |
5 |
26 |
27 |
30 |
--------------------------------------------------------------------------------
/src/components/AppSpinner.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
21 |
22 |
159 |
--------------------------------------------------------------------------------
/src/components/CategoryList.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
8 |
9 |
10 |
11 |
26 |
27 |
30 |
--------------------------------------------------------------------------------
/src/components/CategoryListItem.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | {{ category.name }}
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
37 |
38 |
41 |
--------------------------------------------------------------------------------
/src/components/ForumList.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
8 |
9 |
10 |
11 |
26 |
27 |
30 |
--------------------------------------------------------------------------------
/src/components/ForumListItem.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
8 | {{forum.name}}
9 |
10 |
{{forum.description}}
11 |
12 |
13 |
14 |
{{threadsCount}}
15 | {{threadsCount === 1 ? 'thread' : 'threads'}}
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
42 |
43 |
46 |
--------------------------------------------------------------------------------
/src/components/PostEditor.vue:
--------------------------------------------------------------------------------
1 |
2 |
18 |
19 |
20 |
95 |
96 |
99 |
--------------------------------------------------------------------------------
/src/components/PostList.vue:
--------------------------------------------------------------------------------
1 |
2 |
9 |
10 |
11 |
26 |
27 |
30 |
--------------------------------------------------------------------------------
/src/components/PostListItem.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
{{user.name}}
5 |
6 |
7 |
8 |
9 |
10 |
{{userThreadsCount}} threads
11 |
{{userPostsCount}} posts
12 |
13 |
14 |
15 |
16 |
17 | {{post.text}}
18 |
19 |
20 |
21 |
28 |
29 |
30 |
31 |
35 |
36 |
37 |
38 |
74 |
75 |
78 |
--------------------------------------------------------------------------------
/src/components/TheNavbar.vue:
--------------------------------------------------------------------------------
1 |
2 |
57 |
58 |
59 |
92 |
93 |
96 |
--------------------------------------------------------------------------------
/src/components/ThreadEditor.vue:
--------------------------------------------------------------------------------
1 |
2 |
32 |
33 |
34 |
90 |
91 |
94 |
--------------------------------------------------------------------------------
/src/components/ThreadList.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
Threads
4 |
9 |
10 |
11 |
12 |
27 |
--------------------------------------------------------------------------------
/src/components/ThreadListItem.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | {{thread.title}}
7 |
8 |
9 |
10 | By {{user.name}}, .
11 |
12 |
13 |
14 |
15 |
16 | {{repliesCount}} replies
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
50 |
--------------------------------------------------------------------------------
/src/components/UserProfileCard.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
{{user.username}}
11 |
12 |
{{user.name}}
13 |
14 |
15 | {{user.bio}}
16 | No bio specified.
17 |
18 |
19 |
{{user.username}} is online
20 |
21 |
22 |
23 | {{userPostsCount}} posts
24 | {{userThreadsCount}} threads
25 |
26 |
27 |
28 |
29 |
{{user.website}}
30 |
31 |
32 |
33 |
Member since june 2003, last visited 4 hours ago
34 |
35 |
36 |
37 |
41 | Edit Profile
42 |
43 |
44 |
45 |
46 |
47 |
48 |
68 |
69 |
72 |
--------------------------------------------------------------------------------
/src/components/UserProfileCardEditor.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
15 |
16 | This field is required
17 | Sorry! This username is taken
18 |
19 |
20 |
21 |
22 |
27 |
28 | The name field is required
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 | {{userPostsCount}} posts
39 | {{userThreadsCount}} threads
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
56 |
57 | This field is required
58 | This in not a valid email address
59 | Sorry! This email is taken
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
Member since june 2003, last visited 4 hours ago
75 |
76 |
77 |
78 |
147 |
148 |
151 |
--------------------------------------------------------------------------------
/src/directives/click-outside.js:
--------------------------------------------------------------------------------
1 | export default {
2 | bind (el, binding) {
3 | el.__ClickOutsideHandler__ = event => {
4 | // check if event's target is the el or contained by el
5 | if (!(el === event.target || el.contains(event.target))) {
6 | binding.value(event)
7 | }
8 | }
9 | document.body.addEventListener('click', el.__ClickOutsideHandler__)
10 | },
11 | unbind (el) {
12 | document.body.removeEventListener('click', el.__ClickOutsideHandler__)
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/src/directives/handle-scroll.js:
--------------------------------------------------------------------------------
1 | export default {
2 | bind (el, binding) {
3 | el.__HandleScrollHandler__ = event => binding.value(event)
4 | document.body.addEventListener('mousewheel', el.__HandleScrollHandler__)
5 | document.body.addEventListener('touchmove', el.__HandleScrollHandler__)
6 | },
7 |
8 | unbind (el) {
9 | document.body.removeEventListener('mousewheel', el.__HandleScrollHandler__)
10 | document.body.removeEventListener('touchmove', el.__HandleScrollHandler__)
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/src/main.js:
--------------------------------------------------------------------------------
1 | // The Vue build version to load with the `import` command
2 | // (runtime-only or standalone) has been set in webpack.base.conf with an alias.
3 | import Vue from 'vue'
4 | import firebase from 'firebase'
5 | import App from './App'
6 | import router from './router'
7 | import store from '@/store'
8 | import AppDate from '@/components/AppDate'
9 | import vuelidate from 'vuelidate'
10 |
11 | Vue.use(vuelidate)
12 | Vue.component('AppDate', AppDate)
13 |
14 | Vue.config.productionTip = false
15 |
16 | // Initialize Firebase
17 | const config = {
18 | apiKey: process.env.FIREBASE_API_KEY,
19 | authDomain: process.env.FIREBASE_AUTH_DOMAIN,
20 | databaseURL: process.env.FIREBASE_DATABASE_URL,
21 | projectId: process.env.FIREBASE_PROJECT_ID,
22 | storageBucket: process.env.FIREBASE_STORAGE_BUCKET,
23 | messagingSenderId: process.env.FIREBASE_MESSAGING_ID
24 | }
25 | firebase.initializeApp(config)
26 |
27 | /* eslint-disable no-new */
28 | new Vue({
29 | el: '#app',
30 | router,
31 | store,
32 | template: '',
33 | components: { App }
34 | })
35 |
--------------------------------------------------------------------------------
/src/mixins/asyncDataStatus.js:
--------------------------------------------------------------------------------
1 | export default {
2 | data () {
3 | return {
4 | asyncDataStatus_ready: false
5 | }
6 | },
7 |
8 | methods: {
9 | asyncDataStatus_fetched () {
10 | this.asyncDataStatus_ready = true
11 | this.$emit('ready')
12 | }
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/src/pages/PageCategory.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
{{ category.name }}
4 |
5 |
6 |
7 |
8 |
45 |
46 |
49 |
--------------------------------------------------------------------------------
/src/pages/PageForum.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
68 |
69 |
74 |
--------------------------------------------------------------------------------
/src/pages/PageHome.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
Welcome to the Forum
4 |
5 |
6 |
7 |
8 |
40 |
--------------------------------------------------------------------------------
/src/pages/PageNotFound.vue:
--------------------------------------------------------------------------------
1 |
2 |
5 |
Not Found
6 |
9 | Ooops, we couldn't find what you are looking for. Why don't you
10 |
11 | go home instead?
12 |
13 |
14 |
15 |
16 |
17 |
24 |
25 |
37 |
--------------------------------------------------------------------------------
/src/pages/PageProfile.vue:
--------------------------------------------------------------------------------
1 |
2 |
24 |
25 |
26 |
64 |
65 |
68 |
--------------------------------------------------------------------------------
/src/pages/PageRegister.vue:
--------------------------------------------------------------------------------
1 |
2 |
79 |
80 |
81 |
150 |
151 |
154 |
--------------------------------------------------------------------------------
/src/pages/PageSignIn.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
100 |
101 |
104 |
--------------------------------------------------------------------------------
/src/pages/PageThreadCreate.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
Create new thread in {{forum.name}}
5 |
6 |
11 |
12 |
13 |
14 |
88 |
89 |
92 |
--------------------------------------------------------------------------------
/src/pages/PageThreadEdit.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
Editing {{thread.title}}
5 |
6 |
13 |
14 |
15 |
16 |
91 |
92 |
95 |
--------------------------------------------------------------------------------
/src/pages/PageThreadShow.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
{{thread.title}}
4 |
5 |
10 | Edit Thread
11 |
12 |
13 |
14 | By {{user.name}}, .
15 | {{repliesCount}} replies by {{contributorsCount}} contributors
16 |
17 |
18 |
22 |
23 | Sign in or
24 | Register to post a reply.
25 |
26 |
27 |
28 |
29 |
102 |
--------------------------------------------------------------------------------
/src/router/index.js:
--------------------------------------------------------------------------------
1 | import Vue from 'vue'
2 | import store from '@/store'
3 | import Router from 'vue-router'
4 | import Home from '@/pages/PageHome'
5 | import ThreadShow from '@/pages/PageThreadShow'
6 | import ThreadCreate from '@/pages/PageThreadCreate'
7 | import ThreadEdit from '@/pages/PageThreadEdit'
8 | import Category from '@/pages/PageCategory'
9 | import Forum from '@/pages/PageForum'
10 | import Profile from '@/pages/PageProfile'
11 | import Register from '@/pages/PageRegister'
12 | import SignIn from '@/pages/PageSignIn'
13 | import NotFound from '@/pages/PageNotFound'
14 | Vue.use(Router)
15 |
16 | const router = new Router({
17 | routes: [
18 | {
19 | path: '/',
20 | name: 'Home',
21 | component: Home
22 | },
23 | {
24 | path: '/category/:id',
25 | name: 'Category',
26 | component: Category,
27 | props: true
28 | },
29 | {
30 | path: '/forum/:id',
31 | name: 'Forum',
32 | component: Forum,
33 | props: true
34 | },
35 | {
36 | path: '/thread/create/:forumId',
37 | name: 'ThreadCreate',
38 | component: ThreadCreate,
39 | props: true,
40 | meta: { requiresAuth: true }
41 | },
42 | {
43 | path: '/thread/:id',
44 | name: 'ThreadShow',
45 | component: ThreadShow,
46 | props: true
47 | },
48 | {
49 | path: '/thread/:id/edit',
50 | name: 'ThreadEdit',
51 | component: ThreadEdit,
52 | props: true,
53 | meta: { requiresAuth: true }
54 | },
55 | {
56 | path: '/me',
57 | name: 'Profile',
58 | component: Profile,
59 | props: true,
60 | meta: { requiresAuth: true }
61 | },
62 | {
63 | path: '/me/edit',
64 | name: 'ProfileEdit',
65 | component: Profile,
66 | props: {edit: true},
67 | meta: { requiresAuth: true }
68 | },
69 | {
70 | path: '/register',
71 | name: 'Register',
72 | component: Register,
73 | meta: { requiresGuest: true }
74 | },
75 | {
76 | path: '/signin',
77 | name: 'SignIn',
78 | component: SignIn,
79 | meta: { requiresGuest: true }
80 | },
81 | {
82 | path: '/logout',
83 | name: 'SignOut',
84 | meta: { requiresAuth: true },
85 | beforeEnter (to, from, next) {
86 | store.dispatch('signOut')
87 | .then(() => next({name: 'Home'}))
88 | }
89 | },
90 | {
91 | path: '*',
92 | name: 'NotFound',
93 | component: NotFound
94 | }
95 | ],
96 | mode: 'history'
97 | })
98 |
99 | router.beforeEach((to, from, next) => {
100 | console.log(`🚦 navigating to ${to.name} from ${from.name}`)
101 |
102 | store.dispatch('auth/initAuthentication')
103 | .then(user => {
104 | if (to.matched.some(route => route.meta.requiresAuth)) {
105 | // protected route
106 | if (user) {
107 | next()
108 | } else {
109 | next({name: 'SignIn', query: {redirectTo: to.path}})
110 | }
111 | } else if (to.matched.some(route => route.meta.requiresGuest)) {
112 | // protected route
113 | if (!user) {
114 | next()
115 | } else {
116 | next({name: 'Home'})
117 | }
118 | } else {
119 | next()
120 | }
121 | })
122 | })
123 |
124 | export default router
125 |
--------------------------------------------------------------------------------
/src/store/actions.js:
--------------------------------------------------------------------------------
1 | import firebase from 'firebase'
2 |
3 | export default {
4 | fetchItem ({state, commit}, {id, emoji, resource}) {
5 | console.log('🔥', emoji, id)
6 | return new Promise((resolve, reject) => {
7 | firebase.database().ref(resource).child(id).once('value', snapshot => {
8 | commit('setItem', {resource, id: snapshot.key, item: snapshot.val()})
9 | resolve(state[resource].items[id])
10 | })
11 | })
12 | },
13 |
14 | fetchItems ({dispatch}, {ids, resource, emoji}) {
15 | ids = Array.isArray(ids) ? ids : Object.keys(ids)
16 | return Promise.all(ids.map(id => dispatch('fetchItem', {id, resource, emoji})))
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/src/store/assetHelpers.js:
--------------------------------------------------------------------------------
1 | import Vue from 'vue'
2 |
3 | export const makeAppendChildToParentMutation = ({parent, child}) =>
4 | (state, {childId, parentId}) => {
5 | const resource = state.items[parentId]
6 | if (!resource[child]) {
7 | Vue.set(resource, child, {})
8 | }
9 | Vue.set(resource[child], childId, childId)
10 | }
11 |
--------------------------------------------------------------------------------
/src/store/getters.js:
--------------------------------------------------------------------------------
1 | export default {
2 |
3 | }
4 |
--------------------------------------------------------------------------------
/src/store/index.js:
--------------------------------------------------------------------------------
1 | import Vue from 'vue'
2 | import Vuex from 'vuex'
3 | import actions from './actions'
4 | import mutations from './mutations'
5 | import getters from './getters'
6 | import categories from './modules/categories'
7 | import forums from './modules/forums'
8 | import threads from './modules/threads'
9 | import posts from './modules/posts'
10 | import users from './modules/users'
11 | import auth from './modules/auth'
12 |
13 | Vue.use(Vuex)
14 |
15 | export default new Vuex.Store({
16 | state: {},
17 | getters,
18 | actions,
19 | mutations,
20 | modules: {
21 | categories,
22 | forums,
23 | threads,
24 | posts,
25 | users,
26 | auth
27 | }
28 | })
29 |
--------------------------------------------------------------------------------
/src/store/modules/auth.js:
--------------------------------------------------------------------------------
1 | import firebase from 'firebase'
2 | export default {
3 | namespaced: true,
4 |
5 | state: {
6 | authId: null,
7 | unsubscribeAuthObserver: null
8 | },
9 |
10 | getters: {
11 | authUser (state, getters, rootState) {
12 | return state.authId ? rootState.users.items[state.authId] : null
13 | }
14 | },
15 |
16 | actions: {
17 | initAuthentication ({dispatch, commit, state}) {
18 | return new Promise((resolve, reject) => {
19 | // unsubscribe observer if already listening
20 | if (state.unsubscribeAuthObserver) {
21 | state.unsubscribeAuthObserver()
22 | }
23 |
24 | const unsubscribe = firebase.auth().onAuthStateChanged(user => {
25 | console.log('👣 the user has changed')
26 | if (user) {
27 | dispatch('fetchAuthUser')
28 | .then(dbUser => resolve(dbUser))
29 | } else {
30 | resolve(null)
31 | }
32 | })
33 | commit('setUnsubscribeAuthObserver', unsubscribe)
34 | })
35 | },
36 |
37 | registerUserWithEmailAndPassword ({dispatch}, {email, name, username, password, avatar = null}) {
38 | return firebase.auth().createUserWithEmailAndPassword(email, password)
39 | .then(user => {
40 | return dispatch('users/createUser', {id: user.uid, email, name, username, password, avatar}, {root: true})
41 | })
42 | .then(() => dispatch('fetchAuthUser'))
43 | },
44 |
45 | signInWithEmailAndPassword (context, {email, password}) {
46 | return firebase.auth().signInWithEmailAndPassword(email, password)
47 | },
48 |
49 | signInWithGoogle ({dispatch}) {
50 | const provider = new firebase.auth.GoogleAuthProvider()
51 | return firebase.auth().signInWithPopup(provider)
52 | .then(data => {
53 | const user = data.user
54 | firebase.database().ref('users').child(user.uid).once('value', snapshot => {
55 | if (!snapshot.exists()) {
56 | return dispatch('users/createUser', {id: user.uid, name: user.displayName, email: user.email, username: user.email, avatar: user.photoURL}, {root: true})
57 | .then(() => dispatch('fetchAuthUser'))
58 | }
59 | })
60 | })
61 | },
62 |
63 | signOut ({commit}) {
64 | return firebase.auth().signOut()
65 | .then(() => {
66 | commit('setAuthId', null)
67 | })
68 | },
69 |
70 | fetchAuthUser ({dispatch, commit}) {
71 | const userId = firebase.auth().currentUser.uid
72 | return new Promise((resolve, reject) => {
73 | // check if user exists in the database
74 | firebase.database().ref('users').child(userId).once('value', snapshot => {
75 | if (snapshot.exists()) {
76 | return dispatch('users/fetchUser', {id: userId}, {root: true})
77 | .then(user => {
78 | commit('setAuthId', userId)
79 | resolve(user)
80 | })
81 | } else {
82 | resolve(null)
83 | }
84 | })
85 | })
86 | }
87 | },
88 |
89 | mutations: {
90 | setAuthId (state, id) {
91 | state.authId = id
92 | },
93 |
94 | setUnsubscribeAuthObserver (state, unsubscribe) {
95 | state.unsubscribeAuthObserver = unsubscribe
96 | }
97 | }
98 | }
99 |
--------------------------------------------------------------------------------
/src/store/modules/categories.js:
--------------------------------------------------------------------------------
1 | import firebase from 'firebase'
2 |
3 | export default {
4 | namespaced: true,
5 |
6 | state: {
7 | items: {}
8 | },
9 |
10 | actions: {
11 | fetchAllCategories ({state, commit}) {
12 | console.log('🔥', '🏷', 'all')
13 | return new Promise((resolve, reject) => {
14 | firebase.database().ref('categories').once('value', snapshot => {
15 | const categoriesObject = snapshot.val()
16 | Object.keys(categoriesObject).forEach(categoryId => {
17 | const category = categoriesObject[categoryId]
18 | commit('setItem', {resource: 'categories', id: categoryId, item: category}, {root: true})
19 | })
20 | resolve(Object.values(state.items))
21 | })
22 | })
23 | },
24 |
25 | fetchCategory: ({dispatch}, {id}) => dispatch('fetchItem', {resource: 'categories', id, emoji: '🏷'}, {root: true}),
26 | fetchCategories: ({dispatch}, {ids}) => dispatch('fetchItems', {resource: 'categories', ids, emoji: '🏷'}, {root: true})
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/src/store/modules/forums.js:
--------------------------------------------------------------------------------
1 | import {makeAppendChildToParentMutation} from '@/store/assetHelpers'
2 |
3 | export default {
4 | namespaced: true,
5 |
6 | state: {
7 | items: {}
8 | },
9 |
10 | actions: {
11 | fetchForum: ({dispatch}, {id}) => dispatch('fetchItem', {resource: 'forums', id, emoji: '🌧'}, {root: true}),
12 | fetchForums: ({dispatch}, {ids}) => dispatch('fetchItems', {resource: 'forums', ids, emoji: '🌧'}, {root: true})
13 | },
14 |
15 | mutations: {
16 | appendThreadToForum: makeAppendChildToParentMutation({parent: 'forums', child: 'threads'})
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/src/store/modules/posts.js:
--------------------------------------------------------------------------------
1 | import Vue from 'vue'
2 | import firebase from 'firebase'
3 |
4 | export default {
5 | namespaced: true,
6 |
7 | state: {
8 | items: {}
9 | },
10 |
11 | actions: {
12 | createPost ({commit, state, rootState}, post) {
13 | const postId = firebase.database().ref('posts').push().key
14 | post.userId = rootState.auth.authId
15 | post.publishedAt = Math.floor(Date.now() / 1000)
16 |
17 | const updates = {}
18 | updates[`posts/${postId}`] = post
19 | updates[`threads/${post.threadId}/posts/${postId}`] = postId
20 | updates[`threads/${post.threadId}/contributors/${post.userId}`] = post.userId
21 | updates[`users/${post.userId}/posts/${postId}`] = postId
22 | firebase.database().ref().update(updates)
23 | .then(() => {
24 | commit('setItem', {resource: 'posts', item: post, id: postId}, {root: true})
25 | commit('threads/appendPostToThread', {parentId: post.threadId, childId: postId}, {root: true})
26 | commit('threads/appendContributorToThread', {parentId: post.threadId, childId: post.userId}, {root: true})
27 | commit('users/appendPostToUser', {parentId: post.userId, childId: postId}, {root: true})
28 | return Promise.resolve(state.items[postId])
29 | })
30 | },
31 |
32 | updatePost ({state, commit, rootState}, {id, text}) {
33 | return new Promise((resolve, reject) => {
34 | const post = state.items[id]
35 | const edited = {
36 | at: Math.floor(Date.now() / 1000),
37 | by: rootState.auth.authId
38 | }
39 |
40 | const updates = {text, edited}
41 | firebase.database().ref('posts').child(id).update(updates)
42 | .then(() => {
43 | commit('setPost', {postId: id, post: {...post, text, edited}})
44 | resolve(post)
45 | })
46 | })
47 | },
48 |
49 | fetchPost: ({dispatch}, {id}) => dispatch('fetchItem', {resource: 'posts', id, emoji: '💬'}, {root: true}),
50 | fetchPosts: ({dispatch}, {ids}) => dispatch('fetchItems', {resource: 'posts', ids, emoji: '💬'}, {root: true})
51 | },
52 |
53 | mutations: {
54 | setPost (state, {post, postId}) {
55 | Vue.set(state.items, postId, post)
56 | }
57 | }
58 | }
59 |
--------------------------------------------------------------------------------
/src/store/modules/threads.js:
--------------------------------------------------------------------------------
1 | import {countObjectProperties} from '@/utils'
2 | import firebase from 'firebase'
3 | import Vue from 'vue'
4 | import {makeAppendChildToParentMutation} from '@/store/assetHelpers'
5 |
6 | export default {
7 | namespaced: true,
8 |
9 | state: {
10 | items: {}
11 | },
12 |
13 | getters: {
14 | threadRepliesCount: state => id => countObjectProperties(state.items[id].posts) - 1
15 | },
16 |
17 | actions: {
18 | createThread ({state, commit, dispatch, rootState}, {text, title, forumId}) {
19 | return new Promise((resolve, reject) => {
20 | const threadId = firebase.database().ref('threads').push().key
21 | const postId = firebase.database().ref('posts').push().key
22 | const userId = rootState.auth.authId
23 | const publishedAt = Math.floor(Date.now() / 1000)
24 |
25 | const thread = {title, forumId, publishedAt, userId, firstPostId: postId, posts: {}}
26 | thread.posts[postId] = postId
27 | const post = {text, publishedAt, threadId, userId}
28 |
29 | const updates = {}
30 | updates[`threads/${threadId}`] = thread
31 | updates[`forums/${forumId}/threads/${threadId}`] = threadId
32 | updates[`users/${userId}/threads/${threadId}`] = threadId
33 |
34 | updates[`posts/${postId}`] = post
35 | updates[`users/${userId}/posts/${postId}`] = postId
36 | firebase.database().ref().update(updates)
37 | .then(() => {
38 | // update thread
39 | commit('setItem', {resource: 'threads', id: threadId, item: thread}, {root: true})
40 | commit('forums/appendThreadToForum', {parentId: forumId, childId: threadId}, {root: true})
41 | commit('users/appendThreadToUser', {parentId: userId, childId: threadId}, {root: true})
42 | // update post
43 | commit('setItem', {resource: 'posts', item: post, id: postId}, {root: true})
44 | commit('appendPostToThread', {parentId: post.threadId, childId: postId})
45 | commit('users/appendPostToUser', {parentId: post.userId, childId: postId}, {root: true})
46 |
47 | resolve(state.items[threadId])
48 | })
49 | })
50 | },
51 |
52 | updateThread ({state, commit, dispatch, rootState}, {title, text, id}) {
53 | return new Promise((resolve, reject) => {
54 | const thread = state.items[id]
55 | const post = rootState.posts.items[thread.firstPostId]
56 |
57 | const edited = {
58 | at: Math.floor(Date.now() / 1000),
59 | by: rootState.auth.authId
60 | }
61 |
62 | const updates = {}
63 | updates[`posts/${thread.firstPostId}/text`] = text
64 | updates[`posts/${thread.firstPostId}/edited`] = edited
65 | updates[`threads/${id}/title`] = title
66 |
67 | firebase.database().ref().update(updates)
68 | .then(() => {
69 | commit('setThread', {thread: {...thread, title}, threadId: id})
70 | commit('posts/setPost', {postId: thread.firstPostId, post: {...post, text, edited}}, {root: true})
71 | resolve(post)
72 | })
73 | })
74 | },
75 | fetchThread: ({dispatch}, {id}) => dispatch('fetchItem', {resource: 'threads', id, emoji: '📄'}, {root: true}),
76 | fetchThreads: ({dispatch}, {ids}) => dispatch('fetchItems', {resource: 'threads', ids, emoji: '🌧'}, {root: true})
77 | },
78 | mutations: {
79 | setThread (state, {thread, threadId}) {
80 | Vue.set(state.items, threadId, thread)
81 | },
82 |
83 | appendPostToThread: makeAppendChildToParentMutation({parent: 'threads', child: 'posts'}),
84 | appendContributorToThread: makeAppendChildToParentMutation({parent: 'threads', child: 'contributors'})
85 | }
86 | }
87 |
--------------------------------------------------------------------------------
/src/store/modules/users.js:
--------------------------------------------------------------------------------
1 | import {countObjectProperties, removeEmptyProperties} from '@/utils'
2 | import firebase from 'firebase'
3 | import Vue from 'vue'
4 | import {makeAppendChildToParentMutation} from '@/store/assetHelpers'
5 |
6 | export default {
7 | namespaced: true,
8 |
9 | state: {
10 | items: {}
11 | },
12 |
13 | getters: {
14 | userPosts: (state, getters, rootState) => id => {
15 | const user = state.items[id]
16 | if (user.posts) {
17 | return Object.values(rootState.posts.items)
18 | .filter(post => post.userId === id)
19 | }
20 | return []
21 | },
22 |
23 | userThreadsCount: state => id => countObjectProperties(state.items[id].threads),
24 | userPostsCount: state => id => countObjectProperties(state.items[id].posts)
25 | },
26 |
27 | actions: {
28 | createUser ({state, commit}, {id, email, name, username, avatar = null}) {
29 | return new Promise((resolve, reject) => {
30 | const registeredAt = Math.floor(Date.now() / 1000)
31 | const usernameLower = username.toLowerCase()
32 | email = email.toLowerCase()
33 | const user = {avatar, email, name, username, usernameLower, registeredAt}
34 | firebase.database().ref('users').child(id).set(user)
35 | .then(() => {
36 | commit('setItem', {resource: 'users', id: id, item: user}, {root: true})
37 | resolve(state.items[id])
38 | })
39 | })
40 | },
41 |
42 | updateUser ({commit}, user) {
43 | const updates = {
44 | avatar: user.avatar,
45 | username: user.username,
46 | name: user.name,
47 | bio: user.bio,
48 | website: user.website,
49 | email: user.email,
50 | location: user.location
51 | }
52 | return new Promise((resolve, reject) => {
53 | firebase.database().ref('users').child(user['.key']).update(removeEmptyProperties(updates))
54 | .then(() => {
55 | commit('setUser', {userId: user['.key'], user})
56 | resolve(user)
57 | })
58 | })
59 | },
60 |
61 | fetchUser: ({dispatch}, {id}) => dispatch('fetchItem', {resource: 'users', id, emoji: '🙋'}, {root: true}),
62 | fetchUsers: ({dispatch}, {ids}) => dispatch('fetchItems', {resource: 'users', ids, emoji: '🙋'}, {root: true})
63 | },
64 |
65 | mutations: {
66 | setUser (state, {user, userId}) {
67 | Vue.set(state.items, userId, user)
68 | },
69 |
70 | appendPostToUser: makeAppendChildToParentMutation({parent: 'users', child: 'posts'}),
71 | appendThreadToUser: makeAppendChildToParentMutation({parent: 'users', child: 'threads'})
72 | }
73 | }
74 |
--------------------------------------------------------------------------------
/src/store/mutations.js:
--------------------------------------------------------------------------------
1 | import Vue from 'vue'
2 |
3 | export default {
4 | setItem (state, {item, id, resource}) {
5 | item['.key'] = id
6 | Vue.set(state[resource].items, id, item)
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/src/utils/index.js:
--------------------------------------------------------------------------------
1 | const countObjectProperties = obj => {
2 | if (typeof obj === 'object') {
3 | return Object.keys(obj).length
4 | }
5 | return 0
6 | }
7 |
8 | const removeEmptyProperties = obj => {
9 | const objCopy = {...obj}
10 | Object.keys(objCopy).forEach(key => {
11 | if ([null, undefined].includes(objCopy[key])) {
12 | delete objCopy[key]
13 | }
14 | })
15 | return objCopy
16 | }
17 |
18 | export {
19 | countObjectProperties,
20 | removeEmptyProperties
21 | }
22 |
--------------------------------------------------------------------------------
/src/utils/validators.js:
--------------------------------------------------------------------------------
1 | import firebase from 'firebase'
2 | import {helpers as vuelidateHelpers} from 'vuelidate/lib/validators'
3 |
4 | export const uniqueUsername = (value) => {
5 | if (!vuelidateHelpers.req(value)) {
6 | return true
7 | }
8 | return new Promise((resolve, reject) => {
9 | firebase.database().ref('users').orderByChild('usernameLower').equalTo(value.toLowerCase())
10 | .once('value', snapshot => resolve(!snapshot.exists()))
11 | })
12 | }
13 |
14 | export const supportedImageFile = (value) => {
15 | if (!vuelidateHelpers.req(value)) {
16 | return true
17 | }
18 | const supported = ['jpg', 'jpeg', 'gif', 'png', 'svg']
19 | const suffix = value.split('.').pop()
20 | return supported.includes(suffix)
21 | }
22 |
23 | export const responseOk = (value) => {
24 | if (!vuelidateHelpers.req(value)) {
25 | return true
26 | }
27 | return new Promise((resolve, reject) => {
28 | fetch(value)
29 | .then(response => resolve(response.ok))
30 | .catch(() => resolve(false))
31 | })
32 | }
33 |
34 | export const uniqueEmail = (value) => {
35 | if (!vuelidateHelpers.req(value)) {
36 | return true
37 | }
38 | return new Promise((resolve, reject) => {
39 | firebase.database().ref('users').orderByChild('email').equalTo(value.toLowerCase())
40 | .once('value', snapshot => resolve(!snapshot.exists()))
41 | })
42 | }
43 |
--------------------------------------------------------------------------------
/static/.gitkeep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/vueschool/vue-master-class/4a2febe07c9873c96c84d5108f03768096af3084/static/.gitkeep
--------------------------------------------------------------------------------