├── logs └── .gitignore ├── src ├── components │ ├── auth │ │ ├── register │ │ │ ├── register.component.scss │ │ │ ├── register.component.html │ │ │ └── register.component.js │ │ ├── auth.component.js │ │ ├── auth.component.html │ │ ├── auth.component.scss │ │ └── login │ │ │ ├── login.component.html │ │ │ ├── login.component.scss │ │ │ └── login.component.js │ ├── not-found │ │ ├── not-found.component.scss │ │ ├── not-found.component.js │ │ └── not-found.component.html │ ├── app.component.scss │ ├── app.component.html │ ├── app.component.js │ └── profile │ │ ├── profile.component.scss │ │ ├── profile.component.html │ │ └── profile.component.js ├── modules │ ├── core │ │ ├── index.js │ │ ├── Component.js │ │ └── Render.js │ └── router │ │ ├── index.js │ │ ├── RouterModule.js │ │ └── RouterFactory.js ├── assets │ ├── images │ │ ├── hg.jpg │ │ ├── hg2.jpg │ │ ├── tea.png │ │ ├── logo.png │ │ ├── favicon.ico │ │ ├── apple-icon.png │ │ ├── favicon-16x16.png │ │ ├── favicon-32x32.png │ │ ├── favicon-96x96.png │ │ ├── ms-icon-70x70.png │ │ ├── default_fm_user.png │ │ ├── default_m_user.png │ │ ├── ms-icon-144x144.png │ │ ├── ms-icon-150x150.png │ │ ├── ms-icon-310x310.png │ │ ├── android-icon-36x36.png │ │ ├── android-icon-48x48.png │ │ ├── android-icon-72x72.png │ │ ├── android-icon-96x96.png │ │ ├── apple-icon-114x114.png │ │ ├── apple-icon-120x120.png │ │ ├── apple-icon-144x144.png │ │ ├── apple-icon-152x152.png │ │ ├── apple-icon-180x180.png │ │ ├── apple-icon-57x57.png │ │ ├── apple-icon-60x60.png │ │ ├── apple-icon-72x72.png │ │ ├── apple-icon-76x76.png │ │ ├── android-icon-144x144.png │ │ ├── android-icon-192x192.png │ │ └── apple-icon-precomposed.png │ ├── fonts │ │ ├── noto-sans-v8-vietnamese_latin-ext_devanagari_greek_cyrillic-ext_cyrillic_latin_greek-ext-700.eot │ │ ├── noto-sans-v8-vietnamese_latin-ext_devanagari_greek_cyrillic-ext_cyrillic_latin_greek-ext-700.ttf │ │ ├── noto-sans-v8-vietnamese_latin-ext_devanagari_greek_cyrillic-ext_cyrillic_latin_greek-ext-700.woff │ │ ├── noto-sans-v8-vietnamese_latin-ext_devanagari_greek_cyrillic-ext_cyrillic_latin_greek-ext-700.woff2 │ │ ├── noto-sans-v8-vietnamese_latin-ext_devanagari_greek_cyrillic-ext_cyrillic_latin_greek-ext-italic.eot │ │ ├── noto-sans-v8-vietnamese_latin-ext_devanagari_greek_cyrillic-ext_cyrillic_latin_greek-ext-italic.ttf │ │ ├── noto-sans-v8-vietnamese_latin-ext_devanagari_greek_cyrillic-ext_cyrillic_latin_greek-ext-italic.woff │ │ ├── noto-sans-v8-vietnamese_latin-ext_devanagari_greek_cyrillic-ext_cyrillic_latin_greek-ext-regular.eot │ │ ├── noto-sans-v8-vietnamese_latin-ext_devanagari_greek_cyrillic-ext_cyrillic_latin_greek-ext-regular.ttf │ │ ├── noto-sans-v8-vietnamese_latin-ext_devanagari_greek_cyrillic-ext_cyrillic_latin_greek-ext-700italic.eot │ │ ├── noto-sans-v8-vietnamese_latin-ext_devanagari_greek_cyrillic-ext_cyrillic_latin_greek-ext-700italic.ttf │ │ ├── noto-sans-v8-vietnamese_latin-ext_devanagari_greek_cyrillic-ext_cyrillic_latin_greek-ext-700italic.woff │ │ ├── noto-sans-v8-vietnamese_latin-ext_devanagari_greek_cyrillic-ext_cyrillic_latin_greek-ext-italic.woff2 │ │ ├── noto-sans-v8-vietnamese_latin-ext_devanagari_greek_cyrillic-ext_cyrillic_latin_greek-ext-regular.woff │ │ ├── noto-sans-v8-vietnamese_latin-ext_devanagari_greek_cyrillic-ext_cyrillic_latin_greek-ext-regular.woff2 │ │ ├── noto-sans-v8-vietnamese_latin-ext_devanagari_greek_cyrillic-ext_cyrillic_latin_greek-ext-700italic.woff2 │ │ └── noto-sans-v8-vietnamese_latin-ext_devanagari_greek_cyrillic-ext_cyrillic_latin_greek-ext-700.svg │ └── css │ │ └── google-noto-sans.css ├── scss │ ├── custom.scss │ ├── styles.scss │ ├── _input-group.scss │ └── _variables.scss ├── config.example.js ├── app.js ├── services │ ├── notification.service.js │ ├── user-data.service.js │ └── auth.service.js ├── manifest.json ├── app-routing.js └── index.html ├── README.md ├── .eslintignore ├── .babelrc ├── webpack.dev.js ├── .gitignore ├── webpack.prod.js ├── package.json ├── webpack.config.js └── .eslintrc.json /logs/.gitignore: -------------------------------------------------------------------------------- 1 | *.log -------------------------------------------------------------------------------- /src/components/auth/register/register.component.scss: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/components/not-found/not-found.component.scss: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/modules/core/index.js: -------------------------------------------------------------------------------- 1 | export * from './Component'; 2 | export * from './Render'; -------------------------------------------------------------------------------- /src/modules/router/index.js: -------------------------------------------------------------------------------- 1 | export * from './RouterModule'; 2 | export * from './RouterFactory'; -------------------------------------------------------------------------------- /src/assets/images/hg.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TeaInside/Tea-Messenger-Web/HEAD/src/assets/images/hg.jpg -------------------------------------------------------------------------------- /src/assets/images/hg2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TeaInside/Tea-Messenger-Web/HEAD/src/assets/images/hg2.jpg -------------------------------------------------------------------------------- /src/assets/images/tea.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TeaInside/Tea-Messenger-Web/HEAD/src/assets/images/tea.png -------------------------------------------------------------------------------- /src/assets/images/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TeaInside/Tea-Messenger-Web/HEAD/src/assets/images/logo.png -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | # Tea Messenger Web 3 | 4 | Web based Tea Messenger Client. Secure, fast and simple chat app. 5 | 6 | 7 | -------------------------------------------------------------------------------- /src/assets/images/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TeaInside/Tea-Messenger-Web/HEAD/src/assets/images/favicon.ico -------------------------------------------------------------------------------- /src/assets/images/apple-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TeaInside/Tea-Messenger-Web/HEAD/src/assets/images/apple-icon.png -------------------------------------------------------------------------------- /src/assets/images/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TeaInside/Tea-Messenger-Web/HEAD/src/assets/images/favicon-16x16.png -------------------------------------------------------------------------------- /src/assets/images/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TeaInside/Tea-Messenger-Web/HEAD/src/assets/images/favicon-32x32.png -------------------------------------------------------------------------------- /src/assets/images/favicon-96x96.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TeaInside/Tea-Messenger-Web/HEAD/src/assets/images/favicon-96x96.png -------------------------------------------------------------------------------- /src/assets/images/ms-icon-70x70.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TeaInside/Tea-Messenger-Web/HEAD/src/assets/images/ms-icon-70x70.png -------------------------------------------------------------------------------- /src/assets/images/default_fm_user.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TeaInside/Tea-Messenger-Web/HEAD/src/assets/images/default_fm_user.png -------------------------------------------------------------------------------- /src/assets/images/default_m_user.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TeaInside/Tea-Messenger-Web/HEAD/src/assets/images/default_m_user.png -------------------------------------------------------------------------------- /src/assets/images/ms-icon-144x144.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TeaInside/Tea-Messenger-Web/HEAD/src/assets/images/ms-icon-144x144.png -------------------------------------------------------------------------------- /src/assets/images/ms-icon-150x150.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TeaInside/Tea-Messenger-Web/HEAD/src/assets/images/ms-icon-150x150.png -------------------------------------------------------------------------------- /src/assets/images/ms-icon-310x310.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TeaInside/Tea-Messenger-Web/HEAD/src/assets/images/ms-icon-310x310.png -------------------------------------------------------------------------------- /src/assets/images/android-icon-36x36.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TeaInside/Tea-Messenger-Web/HEAD/src/assets/images/android-icon-36x36.png -------------------------------------------------------------------------------- /src/assets/images/android-icon-48x48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TeaInside/Tea-Messenger-Web/HEAD/src/assets/images/android-icon-48x48.png -------------------------------------------------------------------------------- /src/assets/images/android-icon-72x72.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TeaInside/Tea-Messenger-Web/HEAD/src/assets/images/android-icon-72x72.png -------------------------------------------------------------------------------- /src/assets/images/android-icon-96x96.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TeaInside/Tea-Messenger-Web/HEAD/src/assets/images/android-icon-96x96.png -------------------------------------------------------------------------------- /src/assets/images/apple-icon-114x114.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TeaInside/Tea-Messenger-Web/HEAD/src/assets/images/apple-icon-114x114.png -------------------------------------------------------------------------------- /src/assets/images/apple-icon-120x120.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TeaInside/Tea-Messenger-Web/HEAD/src/assets/images/apple-icon-120x120.png -------------------------------------------------------------------------------- /src/assets/images/apple-icon-144x144.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TeaInside/Tea-Messenger-Web/HEAD/src/assets/images/apple-icon-144x144.png -------------------------------------------------------------------------------- /src/assets/images/apple-icon-152x152.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TeaInside/Tea-Messenger-Web/HEAD/src/assets/images/apple-icon-152x152.png -------------------------------------------------------------------------------- /src/assets/images/apple-icon-180x180.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TeaInside/Tea-Messenger-Web/HEAD/src/assets/images/apple-icon-180x180.png -------------------------------------------------------------------------------- /src/assets/images/apple-icon-57x57.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TeaInside/Tea-Messenger-Web/HEAD/src/assets/images/apple-icon-57x57.png -------------------------------------------------------------------------------- /src/assets/images/apple-icon-60x60.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TeaInside/Tea-Messenger-Web/HEAD/src/assets/images/apple-icon-60x60.png -------------------------------------------------------------------------------- /src/assets/images/apple-icon-72x72.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TeaInside/Tea-Messenger-Web/HEAD/src/assets/images/apple-icon-72x72.png -------------------------------------------------------------------------------- /src/assets/images/apple-icon-76x76.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TeaInside/Tea-Messenger-Web/HEAD/src/assets/images/apple-icon-76x76.png -------------------------------------------------------------------------------- /src/assets/images/android-icon-144x144.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TeaInside/Tea-Messenger-Web/HEAD/src/assets/images/android-icon-144x144.png -------------------------------------------------------------------------------- /src/assets/images/android-icon-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TeaInside/Tea-Messenger-Web/HEAD/src/assets/images/android-icon-192x192.png -------------------------------------------------------------------------------- /src/assets/images/apple-icon-precomposed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TeaInside/Tea-Messenger-Web/HEAD/src/assets/images/apple-icon-precomposed.png -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | 2 | # config 3 | /webpack.* 4 | 5 | # compiled output 6 | /dist 7 | /public 8 | /public_old 9 | /tmp 10 | 11 | # dependencies 12 | /node_modules/ -------------------------------------------------------------------------------- /src/scss/custom.scss: -------------------------------------------------------------------------------- 1 | @import "node_modules/bootstrap/scss/functions"; 2 | @import "node_modules/bootstrap/scss/mixins"; 3 | @import "./variables"; 4 | @import "./input-group"; -------------------------------------------------------------------------------- /src/config.example.js: -------------------------------------------------------------------------------- 1 | export const Configs = { 2 | "apiUrl": "http://api-domain.com/", 3 | "apiEndpoints": { 4 | "login": "login", 5 | "register": "register", 6 | "captcha": "captcha" 7 | } 8 | } -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [["@babel/preset-env", {"modules": false}]], 3 | "plugins": [ 4 | ["@babel/plugin-proposal-decorators", { "legacy": true }], 5 | "@babel/plugin-syntax-dynamic-import", 6 | "@babel/plugin-proposal-class-properties" 7 | ] 8 | } -------------------------------------------------------------------------------- /src/app.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @author TeaInside 3 | * @version 0.0.1 4 | * @license MIT 5 | */ 6 | 7 | import { router } from './app-routing'; 8 | import 'bootstrap'; 9 | 10 | document.addEventListener('DOMContentLoaded', () => { 11 | router.load(); 12 | }); -------------------------------------------------------------------------------- /src/assets/fonts/noto-sans-v8-vietnamese_latin-ext_devanagari_greek_cyrillic-ext_cyrillic_latin_greek-ext-700.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TeaInside/Tea-Messenger-Web/HEAD/src/assets/fonts/noto-sans-v8-vietnamese_latin-ext_devanagari_greek_cyrillic-ext_cyrillic_latin_greek-ext-700.eot -------------------------------------------------------------------------------- /src/assets/fonts/noto-sans-v8-vietnamese_latin-ext_devanagari_greek_cyrillic-ext_cyrillic_latin_greek-ext-700.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TeaInside/Tea-Messenger-Web/HEAD/src/assets/fonts/noto-sans-v8-vietnamese_latin-ext_devanagari_greek_cyrillic-ext_cyrillic_latin_greek-ext-700.ttf -------------------------------------------------------------------------------- /src/assets/fonts/noto-sans-v8-vietnamese_latin-ext_devanagari_greek_cyrillic-ext_cyrillic_latin_greek-ext-700.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TeaInside/Tea-Messenger-Web/HEAD/src/assets/fonts/noto-sans-v8-vietnamese_latin-ext_devanagari_greek_cyrillic-ext_cyrillic_latin_greek-ext-700.woff -------------------------------------------------------------------------------- /src/assets/fonts/noto-sans-v8-vietnamese_latin-ext_devanagari_greek_cyrillic-ext_cyrillic_latin_greek-ext-700.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TeaInside/Tea-Messenger-Web/HEAD/src/assets/fonts/noto-sans-v8-vietnamese_latin-ext_devanagari_greek_cyrillic-ext_cyrillic_latin_greek-ext-700.woff2 -------------------------------------------------------------------------------- /src/assets/fonts/noto-sans-v8-vietnamese_latin-ext_devanagari_greek_cyrillic-ext_cyrillic_latin_greek-ext-italic.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TeaInside/Tea-Messenger-Web/HEAD/src/assets/fonts/noto-sans-v8-vietnamese_latin-ext_devanagari_greek_cyrillic-ext_cyrillic_latin_greek-ext-italic.eot -------------------------------------------------------------------------------- /src/assets/fonts/noto-sans-v8-vietnamese_latin-ext_devanagari_greek_cyrillic-ext_cyrillic_latin_greek-ext-italic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TeaInside/Tea-Messenger-Web/HEAD/src/assets/fonts/noto-sans-v8-vietnamese_latin-ext_devanagari_greek_cyrillic-ext_cyrillic_latin_greek-ext-italic.ttf -------------------------------------------------------------------------------- /src/assets/fonts/noto-sans-v8-vietnamese_latin-ext_devanagari_greek_cyrillic-ext_cyrillic_latin_greek-ext-italic.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TeaInside/Tea-Messenger-Web/HEAD/src/assets/fonts/noto-sans-v8-vietnamese_latin-ext_devanagari_greek_cyrillic-ext_cyrillic_latin_greek-ext-italic.woff -------------------------------------------------------------------------------- /src/assets/fonts/noto-sans-v8-vietnamese_latin-ext_devanagari_greek_cyrillic-ext_cyrillic_latin_greek-ext-regular.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TeaInside/Tea-Messenger-Web/HEAD/src/assets/fonts/noto-sans-v8-vietnamese_latin-ext_devanagari_greek_cyrillic-ext_cyrillic_latin_greek-ext-regular.eot -------------------------------------------------------------------------------- /src/assets/fonts/noto-sans-v8-vietnamese_latin-ext_devanagari_greek_cyrillic-ext_cyrillic_latin_greek-ext-regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TeaInside/Tea-Messenger-Web/HEAD/src/assets/fonts/noto-sans-v8-vietnamese_latin-ext_devanagari_greek_cyrillic-ext_cyrillic_latin_greek-ext-regular.ttf -------------------------------------------------------------------------------- /src/assets/fonts/noto-sans-v8-vietnamese_latin-ext_devanagari_greek_cyrillic-ext_cyrillic_latin_greek-ext-700italic.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TeaInside/Tea-Messenger-Web/HEAD/src/assets/fonts/noto-sans-v8-vietnamese_latin-ext_devanagari_greek_cyrillic-ext_cyrillic_latin_greek-ext-700italic.eot -------------------------------------------------------------------------------- /src/assets/fonts/noto-sans-v8-vietnamese_latin-ext_devanagari_greek_cyrillic-ext_cyrillic_latin_greek-ext-700italic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TeaInside/Tea-Messenger-Web/HEAD/src/assets/fonts/noto-sans-v8-vietnamese_latin-ext_devanagari_greek_cyrillic-ext_cyrillic_latin_greek-ext-700italic.ttf -------------------------------------------------------------------------------- /src/assets/fonts/noto-sans-v8-vietnamese_latin-ext_devanagari_greek_cyrillic-ext_cyrillic_latin_greek-ext-700italic.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TeaInside/Tea-Messenger-Web/HEAD/src/assets/fonts/noto-sans-v8-vietnamese_latin-ext_devanagari_greek_cyrillic-ext_cyrillic_latin_greek-ext-700italic.woff -------------------------------------------------------------------------------- /src/assets/fonts/noto-sans-v8-vietnamese_latin-ext_devanagari_greek_cyrillic-ext_cyrillic_latin_greek-ext-italic.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TeaInside/Tea-Messenger-Web/HEAD/src/assets/fonts/noto-sans-v8-vietnamese_latin-ext_devanagari_greek_cyrillic-ext_cyrillic_latin_greek-ext-italic.woff2 -------------------------------------------------------------------------------- /src/assets/fonts/noto-sans-v8-vietnamese_latin-ext_devanagari_greek_cyrillic-ext_cyrillic_latin_greek-ext-regular.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TeaInside/Tea-Messenger-Web/HEAD/src/assets/fonts/noto-sans-v8-vietnamese_latin-ext_devanagari_greek_cyrillic-ext_cyrillic_latin_greek-ext-regular.woff -------------------------------------------------------------------------------- /src/assets/fonts/noto-sans-v8-vietnamese_latin-ext_devanagari_greek_cyrillic-ext_cyrillic_latin_greek-ext-regular.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TeaInside/Tea-Messenger-Web/HEAD/src/assets/fonts/noto-sans-v8-vietnamese_latin-ext_devanagari_greek_cyrillic-ext_cyrillic_latin_greek-ext-regular.woff2 -------------------------------------------------------------------------------- /src/assets/fonts/noto-sans-v8-vietnamese_latin-ext_devanagari_greek_cyrillic-ext_cyrillic_latin_greek-ext-700italic.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TeaInside/Tea-Messenger-Web/HEAD/src/assets/fonts/noto-sans-v8-vietnamese_latin-ext_devanagari_greek_cyrillic-ext_cyrillic_latin_greek-ext-700italic.woff2 -------------------------------------------------------------------------------- /webpack.dev.js: -------------------------------------------------------------------------------- 1 | const webpack = require('webpack'); 2 | const merge = require('webpack-merge'); 3 | const baseConfig = require('./webpack.config.js'); 4 | 5 | module.exports = merge(baseConfig, { 6 | devtool: 'eval', 7 | mode: 'development', 8 | plugins: [ 9 | new webpack.DefinePlugin({ 10 | 'process.env': { 11 | NODE_ENV: JSON.stringify('development') 12 | }, 13 | }) 14 | ] 15 | }); -------------------------------------------------------------------------------- /src/components/auth/auth.component.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @author TeaInside 3 | * @version 0.0.1 4 | * @license MIT 5 | */ 6 | 7 | import { Component } from 'Modules/core'; 8 | 9 | @Component({ 10 | 'selector': 'app-root', 11 | 'template': require('./auth.component.html'), 12 | 'styles': [require('./auth.component.scss')] 13 | }) 14 | export class AuthComponent { 15 | constructor() { 16 | 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/components/not-found/not-found.component.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @author TeaInside 3 | * @version 0.0.1 4 | * @license MIT 5 | */ 6 | 7 | import { Component } from 'Modules/core'; 8 | 9 | @Component({ 10 | 'selector': 'app-root', 11 | 'template': require('./not-found.component.html'), 12 | 'styles': [require('./not-found.component.scss')] 13 | }) 14 | export class NotFoundComponent { 15 | constructor() { 16 | 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/components/app.component.scss: -------------------------------------------------------------------------------- 1 | @import "node_modules/bootstrap/scss/functions"; 2 | @import "node_modules/bootstrap/scss/mixins"; 3 | @import "../scss/variables"; 4 | 5 | .app-home { 6 | height: 100%; 7 | font-family: "Noto Sans", sans-serif; 8 | display: flex; 9 | justify-content: center; 10 | align-items: center; 11 | } 12 | 13 | .logo-image { 14 | width: 100px; 15 | height: 100px; 16 | box-shadow: $box-shadow !important; 17 | border-radius: 50%; 18 | } -------------------------------------------------------------------------------- /src/modules/core/Component.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @author TeaInside 3 | * @version 0.0.1 4 | * @license MIT 5 | */ 6 | 7 | /** 8 | * Add Component Decorator 9 | * 10 | * @param {Object} Component metadata objects 11 | * @return {Object} Component metadata 12 | */ 13 | export function Component(componentMetadata) { 14 | return function(data) { 15 | data.selector = componentMetadata.selector; 16 | data.template = componentMetadata.template; 17 | data.styles = componentMetadata.styles; 18 | return data; 19 | } 20 | } -------------------------------------------------------------------------------- /src/components/app.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 | 5 |
6 |
7 |

Tea Messenger Web

8 |
9 |
10 | Join with us! 11 |
12 |
13 |
-------------------------------------------------------------------------------- /src/components/not-found/not-found.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 | 5 |
6 |
7 |

What are you looking for?

8 |
9 |
10 | Back to home 11 |
12 |
13 |
-------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # config 3 | /src/config.js 4 | webpack.test.js 5 | 6 | # compiled output 7 | /dist 8 | /public 9 | /public_old 10 | /tmp 11 | 12 | # dependencies 13 | /node_modules/ 14 | 15 | # IDEs and editors 16 | /.idea 17 | .project 18 | .classpath 19 | .c9/ 20 | *.launch 21 | .settings/ 22 | *.sublime-workspace 23 | *.sublime-project 24 | 25 | # IDE - VSCode 26 | .vscode/* 27 | !.vscode/settings.json 28 | !.vscode/tasks.json 29 | !.vscode/launch.json 30 | !.vscode/extensions.json 31 | 32 | # misc 33 | /.sass-cache 34 | /.phpintel 35 | /coverage 36 | npm-debug.log 37 | yarn-error.log 38 | testem.log 39 | /typings 40 | /src/config.js -------------------------------------------------------------------------------- /src/components/auth/auth.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |
5 | 6 |

Tea Messenger Web

7 |
8 |
9 |
10 |
11 |
12 |
13 |
Made with by TeaInside
14 |
15 |
-------------------------------------------------------------------------------- /src/components/app.component.js: -------------------------------------------------------------------------------- 1 | import { Component } from 'Modules/core'; 2 | 3 | // Component's class decorator 4 | @Component({ 5 | 'selector': 'app-root', 6 | 'template': require('./app.component.html'), 7 | 'styles': [require('./app.component.scss')] 8 | }) 9 | export class AppComponent { 10 | constructor() { 11 | 12 | } 13 | 14 | title = 'Tea Messenger'; 15 | 16 | // This method will be called once after constructing 17 | // the class component 18 | onInit() { 19 | 20 | } 21 | 22 | // This method will be called once after html has 23 | // appended to the DOM 24 | onAfterView() { 25 | 26 | } 27 | 28 | // This method will be called once when leaving the route 29 | onDestroy() { 30 | 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/scss/styles.scss: -------------------------------------------------------------------------------- 1 | @import "node_modules/bootstrap/scss/functions"; 2 | @import "node_modules/bootstrap/scss/mixins"; 3 | @import "./variables"; 4 | @import "node_modules/bootstrap/scss/bootstrap"; 5 | 6 | html { 7 | font-size: $font-size-base; 8 | } 9 | 10 | body,html { 11 | height: 100%; 12 | } 13 | 14 | body { 15 | font-family: "Noto-Sans", sans-serif; 16 | font-size: $font-size-base; 17 | font-weight: $font-weight-base; 18 | line-height: $line-height-base; 19 | color: $body-color; 20 | background-color: $body-bg; 21 | overflow-x: hidden; 22 | } 23 | 24 | a { 25 | color: $link-color; 26 | text-decoration: none; 27 | transition: all 0.5s ease; 28 | 29 | &:focus, &:hover { 30 | color: $link-hover-color; 31 | text-decoration: underline; 32 | } 33 | } 34 | 35 | a, i, span { 36 | display: inline-block; 37 | text-decoration: none; 38 | } 39 | 40 | a:hover, a:focus, i:hover, i:focus, span:hover, span:focus { 41 | text-decoration: none; 42 | } -------------------------------------------------------------------------------- /src/components/auth/auth.component.scss: -------------------------------------------------------------------------------- 1 | @import "node_modules/bootstrap/scss/functions"; 2 | @import "node_modules/bootstrap/scss/mixins"; 3 | @import "../../scss/variables"; 4 | 5 | .app-auth { 6 | height: 100%; 7 | font-family: "Noto Sans", sans-serif; 8 | display: flex; 9 | justify-content: center; 10 | align-items: center; 11 | 12 | .box-wrapper { 13 | border-radius: $border-radius; 14 | min-width: 300px; 15 | max-width: 600px; 16 | padding: 3rem; 17 | .box-title { 18 | margin-bottom: 2rem; 19 | text-align: center; 20 | } 21 | } 22 | 23 | .footer { 24 | text-align: center; 25 | margin-top: 2rem; 26 | 27 | .icon { 28 | color: #6D4C41; 29 | } 30 | } 31 | 32 | @include media-breakpoint-up(lg) { 33 | .box-wrapper { 34 | box-shadow: 0px 0px 20px 0px #ddd; 35 | } 36 | } 37 | 38 | @include media-breakpoint-down(md) { 39 | .box-wrapper { 40 | border: 0; 41 | padding: 1rem; 42 | } 43 | } 44 | } -------------------------------------------------------------------------------- /src/services/notification.service.js: -------------------------------------------------------------------------------- 1 | import Noty from 'noty'; 2 | 3 | export class NotificationService { 4 | constructor() { 5 | Noty.overrideDefaults({ 6 | layout : 'bottomRight', 7 | closeWith: ['click', 'button'], 8 | theme : 'mint', 9 | timeout : 3000, 10 | animation: { 11 | open : 'animated bounceInUp', 12 | close: 'animated fadeOut' 13 | } 14 | }); 15 | } 16 | 17 | create(options) { 18 | return new Noty(options); 19 | } 20 | 21 | showSuccess(text) { 22 | return this.create({ 23 | type : 'success', 24 | text : text 25 | }); 26 | } 27 | 28 | showWarning(text) { 29 | return this.create({ 30 | type : 'warning', 31 | text : text 32 | }); 33 | } 34 | 35 | showError(text) { 36 | return this.create({ 37 | type : 'error', 38 | text : text 39 | }); 40 | } 41 | 42 | showInfo(text) { 43 | return this.create({ 44 | type : 'info', 45 | text : text 46 | }); 47 | } 48 | } -------------------------------------------------------------------------------- /src/modules/router/RouterModule.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @author TeaInside 3 | * @version 0.0.1 4 | * @license MIT 5 | */ 6 | 7 | import Navigo from 'navigo'; 8 | import _ from 'lodash'; 9 | import { Render } from '../core'; 10 | import { RouterFactory } from './RouterFactory'; 11 | 12 | const origin = location.origin; 13 | const path = document.querySelector('base').getAttribute('href'); 14 | const routerFactory = new RouterFactory(); 15 | const Router = new Navigo(origin + path, false, '#!'); 16 | 17 | class RouterModule { 18 | constructor(routes) { 19 | this.routes = routes; 20 | // this.urlParameter = ''; 21 | // this.urlQuery = ''; 22 | } 23 | 24 | /** 25 | * Map the given routes and register 26 | * 27 | * @return Array 28 | */ 29 | load() { 30 | return _.map(this.routes, routerFactory.registerRoute); 31 | } 32 | 33 | // getUrlParameter() { 34 | // return this.urlParameter; 35 | // } 36 | 37 | // getUrlQuery() { 38 | // return this.urlQuery; 39 | // } 40 | } 41 | 42 | export { Router, RouterModule }; -------------------------------------------------------------------------------- /src/services/user-data.service.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @author TeaInside 3 | * @version 0.0.1 4 | * @license MIT 5 | */ 6 | 7 | import { Configs } from '../config.js'; 8 | 9 | export class UserDataService { 10 | constructor() { 11 | this.apiUrl = new URL(Configs.apiUrl); 12 | this.apiEp = Configs.apiEndpoints; 13 | } 14 | 15 | /** 16 | * @return {Promise} 17 | */ 18 | getUserData() { 19 | let params = new URLSearchParams(this.apiUrl); 20 | params.append('action', 'get_user_info'); 21 | 22 | this.apiUrl.pathname = this.apiEp.profile; 23 | this.apiUrl.search = params; 24 | 25 | let token = this.getToken(); 26 | let request = new Request(this.apiUrl.href, { 27 | method: 'GET', 28 | mode: 'cors', 29 | credentials: "same-origin", 30 | headers: { 31 | "Authorization": `Bearer ${token}` 32 | } 33 | }); 34 | 35 | return fetch(request); 36 | } 37 | 38 | /** 39 | * @return {String}; 40 | */ 41 | getToken() { 42 | let token = localStorage.getItem('token_session'); 43 | return token; 44 | } 45 | } -------------------------------------------------------------------------------- /src/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Tea Messenger Web", 3 | "start_url": "/", 4 | "icons": [ 5 | { 6 | "src": "\/assets\/images\/android-icon-36x36.png", 7 | "sizes": "36x36", 8 | "type": "image\/png", 9 | "density": "0.75" 10 | }, 11 | { 12 | "src": "\/assets\/images\/android-icon-48x48.png", 13 | "sizes": "48x48", 14 | "type": "image\/png", 15 | "density": "1.0" 16 | }, 17 | { 18 | "src": "\/assets\/images\/android-icon-72x72.png", 19 | "sizes": "72x72", 20 | "type": "image\/png", 21 | "density": "1.5" 22 | }, 23 | { 24 | "src": "\/assets\/images\/android-icon-96x96.png", 25 | "sizes": "96x96", 26 | "type": "image\/png", 27 | "density": "2.0" 28 | }, 29 | { 30 | "src": "\/assets\/images\/android-icon-144x144.png", 31 | "sizes": "144x144", 32 | "type": "image\/png", 33 | "density": "3.0" 34 | }, 35 | { 36 | "src": "\/assets\/images\/android-icon-192x192.png", 37 | "sizes": "192x192", 38 | "type": "image\/png", 39 | "density": "4.0" 40 | } 41 | ], 42 | "theme_color": "#27ae60", 43 | "display": "standalone" 44 | } -------------------------------------------------------------------------------- /src/app-routing.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @author TeaInside 3 | * @version 0.0.1 4 | * @license MIT 5 | */ 6 | 7 | import { RouterModule } from 'Modules/router'; 8 | import { AppComponent } from 'Components/app.component'; 9 | import { NotFoundComponent } from 'Components/not-found/not-found.component'; 10 | import { AuthComponent } from 'Components/auth/auth.component'; 11 | import { LoginComponent } from 'Components/auth/login/login.component'; 12 | import { RegisterComponent } from 'Components/auth/register/register.component'; 13 | import { ProfileComponent } from 'Components/profile/profile.component'; 14 | 15 | const routes = [ 16 | { 17 | path: '/', 18 | component: AppComponent 19 | }, 20 | { 21 | path: 'auth', 22 | component: AuthComponent, 23 | redirectTo: 'auth/login', 24 | children: [ 25 | { 26 | path: 'login', 27 | component: LoginComponent 28 | }, 29 | { 30 | path: 'register', 31 | component: RegisterComponent 32 | } 33 | ] 34 | }, 35 | { 36 | path: 'profile', 37 | component: ProfileComponent 38 | }, 39 | { 40 | path: '**', 41 | component: NotFoundComponent 42 | } 43 | ]; 44 | 45 | export const router = new RouterModule(routes); -------------------------------------------------------------------------------- /webpack.prod.js: -------------------------------------------------------------------------------- 1 | const webpack = require('webpack'); 2 | const merge = require('webpack-merge'); 3 | const baseConfig = require('./webpack.config.js'); 4 | const OptimizeCSSAssetsPlugin = require('optimize-css-assets-webpack-plugin'); 5 | const UglifyJsPlugin = require('uglifyjs-webpack-plugin'); 6 | 7 | module.exports = merge(baseConfig, { 8 | mode: 'production', 9 | devtool: 'cheap-module-source-map', 10 | devServer: { 11 | compress: true 12 | }, 13 | plugins: [ 14 | new webpack.DefinePlugin({ 15 | 'process.env': { 16 | NODE_ENV: JSON.stringify('production') 17 | }, 18 | }) 19 | ], 20 | optimization: { 21 | runtimeChunk: false, 22 | splitChunks: { 23 | minSize: 30000, 24 | maxSize: 50000, 25 | minChunks: 1, 26 | cacheGroups: { 27 | default: false, 28 | vendor: { 29 | test: /[\\/]node_modules[\\/]/, 30 | name: 'vendor', 31 | chunks: 'all', 32 | reuseExistingChunk: true 33 | } 34 | } 35 | }, 36 | minimize: true, 37 | minimizer: [ 38 | new OptimizeCSSAssetsPlugin(), 39 | new UglifyJsPlugin({ 40 | sourceMap: true, 41 | uglifyOptions: { 42 | compress: true 43 | } 44 | }) 45 | ] 46 | }, 47 | performance: { 48 | hints: false, 49 | maxAssetSize: 200000 50 | } 51 | }); -------------------------------------------------------------------------------- /src/modules/core/Render.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @author TeaInside 3 | * @version 0.0.1 4 | * @license MIT 5 | */ 6 | 7 | import DOMPurify from 'dompurify'; 8 | 9 | /** 10 | * @param {String} HTML template string 11 | * @return {String} Sanitized HTML template 12 | */ 13 | export const sanitizeHTML = (dirty) => { 14 | let config = { 15 | ADD_TAGS: ['router-outlet'], 16 | ADD_ATTR: ['onclick', 'onload', 'onunload', 'onmouseover'] 17 | }; 18 | return DOMPurify.sanitize(dirty, config); 19 | } 20 | 21 | export class Render { 22 | /** 23 | * @param {String} Element ID 24 | * @param {String} HTML template string 25 | * @return {String} Sanitized HTML template 26 | */ 27 | appendToDOM(element, template) { 28 | let rootElement = document.getElementById(element); 29 | let sanitizedTemplate = sanitizeHTML(template); 30 | 31 | try { 32 | this.clearElement(element); 33 | rootElement.innerHTML = sanitizedTemplate; 34 | } catch(e) { console.error(e)} 35 | 36 | return sanitizedTemplate; 37 | } 38 | 39 | /** 40 | * @param {String} Element ID 41 | * @return {void} 42 | */ 43 | clearElement(element) { 44 | let dom = document.getElementById(element); 45 | try { 46 | while (dom.hasChildNodes()) { 47 | dom.removeChild(dom.firstChild); 48 | } 49 | } catch(e) {console.error(e)} 50 | 51 | } 52 | } -------------------------------------------------------------------------------- /src/components/auth/login/login.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | 4 | 5 | 6 |
7 | 8 |
9 |
10 | 11 |
12 | 13 | 14 | 15 |
16 | 17 |
18 |
19 | 20 |
21 |
22 | 23 | 26 |
27 |
28 | 29 |

Don't have account?

30 | Register 31 |
-------------------------------------------------------------------------------- /src/components/profile/profile.component.scss: -------------------------------------------------------------------------------- 1 | @import "node_modules/bootstrap/scss/functions"; 2 | @import "node_modules/bootstrap/scss/mixins"; 3 | @import "../../scss/variables"; 4 | 5 | .app-profile { 6 | font-family: "Noto Sans", sans-serif; 7 | background-color: $white; 8 | 9 | .component-title { 10 | background-color: $primary; 11 | color: $white; 12 | font-weight: $font-weight-bold; 13 | 14 | .back-button { 15 | display: flex; 16 | align-items: center; 17 | justify-content: center; 18 | color: $white; 19 | 20 | i { 21 | font-size: $font-size-lg; 22 | } 23 | 24 | &:hover { 25 | background-color: darken($primary, 5%); 26 | } 27 | } 28 | 29 | .sign-out-btn { 30 | @extend .back-button; 31 | border-radius: 0; 32 | } 33 | 34 | .title { 35 | padding: 1rem; 36 | text-align: left; 37 | } 38 | } 39 | 40 | .section { 41 | padding: 1rem; 42 | 43 | &:not(:last-child) { 44 | border-bottom: 1px solid $gray-400; 45 | } 46 | } 47 | 48 | .section-profile { 49 | display: flex; 50 | align-items: center; 51 | 52 | .profile-name { 53 | margin-left: 2rem; 54 | 55 | span { 56 | max-width: 200px; 57 | display: inline; 58 | } 59 | } 60 | } 61 | 62 | .section-details { 63 | .icon { 64 | text-align: center; 65 | color: $gray-400; 66 | margin-top: .5rem; 67 | } 68 | 69 | .details { 70 | margin-bottom: 1rem; 71 | } 72 | 73 | .main { 74 | margin: 0; 75 | margin-top: -5px; 76 | } 77 | 78 | .description { 79 | @extend .main; 80 | margin: 0; 81 | font-size: $font-size-sm; 82 | color: $gray-600; 83 | } 84 | } 85 | } -------------------------------------------------------------------------------- /src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | Tea Messenger Web 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /src/components/auth/login/login.component.scss: -------------------------------------------------------------------------------- 1 | @import "node_modules/bootstrap/scss/functions"; 2 | @import "node_modules/bootstrap/scss/mixins"; 3 | @import "../../../scss/variables"; 4 | 5 | /*.box-title { 6 | padding: 3rem; 7 | background-color: #13c100; 8 | color: $white; 9 | 10 | .icon { 11 | margin-right: 1rem; 12 | font-size: $font-size-lg; 13 | width: 20px; 14 | height: 20px; 15 | } 16 | 17 | .divider { 18 | width: 20%; 19 | height: 0; 20 | margin: 2rem 0; 21 | overflow: hidden; 22 | border-top: 1px solid $white; 23 | } 24 | 25 | h3 { 26 | font-weight: bold; 27 | } 28 | } 29 | 30 | .box-form { 31 | background-color: $white; 32 | min-height: inherit; 33 | 34 | .tab-content { 35 | padding: 3rem; 36 | } 37 | 38 | .tab-pane { 39 | padding: 0; 40 | } 41 | 42 | label { 43 | font-weight: 900; 44 | display: block; 45 | } 46 | } 47 | 48 | .captcha { 49 | text-align: center; 50 | 51 | & > img { 52 | margin-bottom: 1rem; 53 | } 54 | 55 | input { 56 | width: 50%; 57 | margin: auto; 58 | } 59 | } 60 | 61 | .form-tabs { 62 | &, .nav-item { 63 | padding: 0; 64 | border: 0; 65 | } 66 | 67 | .nav-link { 68 | padding: 1rem; 69 | font-size: $font-size-base; 70 | text-align: center; 71 | @include border-radius(0); 72 | border: 0; 73 | border-bottom: $border-width solid $border-color; 74 | 75 | &[aria-selected="true"] { 76 | background-color: $gray-100; 77 | font-weight: 900; 78 | color: $primary; 79 | border-bottom: $border-width solid $primary; 80 | } 81 | } 82 | } 83 | 84 | .footer { 85 | margin-top: 1rem; 86 | margin-bottom: 1rem; 87 | justify-content: center; 88 | width: 100%; 89 | 90 | p { 91 | margin: 0; 92 | } 93 | } 94 | 95 | @include media-breakpoint-down(md) { 96 | .app-login { 97 | .box-wrapper-outer { 98 | height: auto; 99 | margin: 0; 100 | border: 0; 101 | } 102 | 103 | .box-wrapper-inner { 104 | margin: 0; 105 | width: 100%; 106 | } 107 | } 108 | } 109 | 110 | @include media-breakpoint-down(sm) { 111 | .box-title { 112 | padding: 2rem; 113 | } 114 | 115 | .box-form { 116 | .tab-content { 117 | padding: 3rem 2rem; 118 | } 119 | } 120 | } 121 | 122 | .btn-lg { 123 | color: $white; 124 | } 125 | 126 | .btn-lg:hover { 127 | color: $white; 128 | }*/ -------------------------------------------------------------------------------- /src/components/profile/profile.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | 4 |
User Profile
5 |
7 |
8 |
9 |
10 | 11 |
12 |
13 |

14 | First 15 | Last 16 |

17 |

18 |

19 | Loading... 20 |
21 |

22 |
23 |
24 |
25 |
26 |
27 | 28 |
29 |
30 |
31 | Loading... 32 |
33 |

34 |

Email

35 |
36 |
37 |
38 |
39 | 40 |
41 |
42 |
43 | Loading... 44 |
45 |

46 |

Gender

47 |
48 |
49 |
50 |
51 |
52 |
Made with by TeaInside
-------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "tea-messenger-web", 3 | "version": "0.0.1", 4 | "description": "Web based Tea Messenger Client. Secure, fast and simple chat app.", 5 | "private": true, 6 | "dependencies": { 7 | "@fortawesome/fontawesome-free": "^5.8.1", 8 | "animate.css": "^3.7.0", 9 | "bootstrap": "^4.3.1", 10 | "dompurify": "^2.0.17", 11 | "jquery": "^3.5.0", 12 | "lodash": "^4.17.21", 13 | "navigo": "^7.1.2", 14 | "noty": "^3.2.0-beta", 15 | "path-browserify": "^1.0.0", 16 | "popper.js": "^1.15.0" 17 | }, 18 | "devDependencies": { 19 | "@babel/core": "^7.4.4", 20 | "@babel/plugin-proposal-class-properties": "^7.4.4", 21 | "@babel/plugin-proposal-decorators": "^7.4.4", 22 | "@babel/plugin-syntax-dynamic-import": "^7.2.0", 23 | "@babel/preset-env": "^7.4.4", 24 | "autoprefixer": "^9.5.1", 25 | "babel-eslint": "^10.0.1", 26 | "babel-loader": "^8.0.5", 27 | "clean-webpack-plugin": "^2.0.1", 28 | "copy-webpack-plugin": "^5.0.3", 29 | "cross-env": "^5.2.0", 30 | "css-loader": "^2.1.1", 31 | "eslint": "^5.16.0", 32 | "eslint-loader": "^2.1.2", 33 | "file-loader": "^3.0.1", 34 | "html-loader": "^0.5.5", 35 | "html-webpack-plugin": "^3.2.0", 36 | "mini-css-extract-plugin": "^0.5.0", 37 | "optimize-css-assets-webpack-plugin": "^5.0.1", 38 | "postcss-loader": "^3.0.0", 39 | "raw-loader": "^2.0.0", 40 | "sass": "^1.20.1", 41 | "sass-loader": "^7.1.0", 42 | "style-loader": "^0.23.1", 43 | "uglifyjs-webpack-plugin": "^2.1.2", 44 | "url-loader": "^1.1.2", 45 | "webpack": "^4.30.0", 46 | "webpack-cli": "^3.3.2", 47 | "webpack-dev-server": "^3.3.1", 48 | "webpack-merge": "^4.2.1" 49 | }, 50 | "browserslist": [ 51 | "> 1%", 52 | "last 10 versions", 53 | "Firefox ESR" 54 | ], 55 | "scripts": { 56 | "build:prod": "cross-env NODE_ENV=production webpack --config=webpack.prod.js", 57 | "build:dev": "cross-env NODE_ENV=development webpack --config=webpack.dev.js", 58 | "prod": "cross-env NODE_ENV=production webpack-dev-server --config=webpack.prod.js", 59 | "dev": "cross-env NODE_ENV=development webpack-dev-server --config=webpack.dev.js", 60 | "lint": "eslint --ignore-path .eslintignore .", 61 | "test": "echo \"Error: no test specified\" && exit 1" 62 | }, 63 | "repository": { 64 | "type": "git", 65 | "url": "git+https://github.com/TeaInside/Tea-Messenger-Web.git" 66 | }, 67 | "author": "TeaInside", 68 | "license": "UNLICENSED", 69 | "bugs": { 70 | "url": "https://github.com/TeaInside/Tea-Messenger-Web/issues" 71 | }, 72 | "homepage": "https://messenger.teainside.org/" 73 | } 74 | -------------------------------------------------------------------------------- /src/components/profile/profile.component.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @author TeaInside 3 | * @version 0.0.1 4 | * @license MIT 5 | */ 6 | 7 | import { Component } from 'Modules/core'; 8 | import { Router } from 'Modules/router'; 9 | 10 | import { AuthService } from 'Services/auth.service'; 11 | import { NotificationService } from 'Services/notification.service'; 12 | import { UserDataService } from 'Services/user-data.service'; 13 | 14 | @Component({ 15 | 'selector': 'app-root', 16 | 'template': require('./profile.component.html'), 17 | 'styles': [require('./profile.component.scss')] 18 | }) 19 | export class ProfileComponent { 20 | constructor() { 21 | this.authService = new AuthService(); 22 | this.notifService = new NotificationService(); 23 | this.userDataService = new UserDataService(); 24 | this.userData = {}; 25 | } 26 | 27 | onInit() { 28 | 29 | } 30 | 31 | onAfterView() { 32 | this.fetchUserData(); 33 | 34 | this.signOut = document.getElementById('sign-out'); 35 | this.signOut.addEventListener('click', () => { 36 | this.authService.logout(); 37 | Router.navigate('/auth/login'); 38 | }); 39 | 40 | if (this.authService.isLoggedIn() === false) { 41 | Router.navigate('/auth/login'); 42 | } 43 | } 44 | 45 | onDestroy() { 46 | this.signOut.removeEventListener('click', () => { 47 | this.authService.logout(); 48 | }); 49 | } 50 | 51 | fetchUserData() { 52 | this.userDataService.getUserData() 53 | .then(response => { 54 | if(response.ok === true) { 55 | response.json().then(result => { 56 | this.appendData(result.data); 57 | }); 58 | } else { 59 | response.json().then(result => { 60 | this.authService.logout(); 61 | Router.navigate('/auth/login'); 62 | }) 63 | } 64 | }) 65 | .catch(error => { 66 | this.notifService.showError('Could not retrieve data from the server. Please try again.').show(); 67 | }); 68 | } 69 | 70 | appendData(data) { 71 | let firstName = document.getElementById('first-name'); 72 | let lastName = document.getElementById('last-name'); 73 | let email = document.getElementById('email'); 74 | let phone = document.getElementById('phone-number'); 75 | let gender = document.getElementById('gender'); 76 | 77 | firstName.innerHTML = data.first_name; 78 | lastName.innerHTML = data.last_name; 79 | email.innerHTML = data.email; 80 | phone.innerHTML = data.phone; 81 | 82 | if (data.gender === 'm') { 83 | gender.innerHTML = 'Male'; 84 | } else { 85 | gender.innerHTML = 'Female'; 86 | } 87 | 88 | let spinner = document.getElementsByClassName('spinner-border'); 89 | for (let i = 0; i < spinner.length; i++) { 90 | spinner[i].classList.add('d-none'); 91 | } 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /src/scss/_input-group.scss: -------------------------------------------------------------------------------- 1 | .input-group { 2 | position: relative; 3 | margin-bottom: 2rem; 4 | background-color: $gray-200; 5 | border-radius: 5px; 6 | 7 | input { 8 | position: relative; 9 | font-size: 16px; 10 | padding: 20px 0px 10px; 11 | margin: 0px 20px; 12 | margin-right: 50px; 13 | display: block; 14 | width: 100%; 15 | border: none; 16 | transition: border 0.2s ease; 17 | background-color: transparent; 18 | } 19 | 20 | input:focus { 21 | outline:none; 22 | } 23 | 24 | .input-label { 25 | color: $gray-800; 26 | font-size: 16px; 27 | font-weight: normal; 28 | position: absolute; 29 | pointer-events: none; 30 | left: 20px; 31 | top: 15px; 32 | transition: 0.2s ease all; 33 | } 34 | 35 | input:focus ~ .input-label, 36 | input:valid ~ .input-label { 37 | top: 6px; 38 | font-size: 12px; 39 | } 40 | 41 | input:focus ~ .input-label { 42 | color: $green; 43 | } 44 | 45 | input.has-prepend ~ .input-label { 46 | left: 50px; 47 | } 48 | 49 | input.has-prepend { 50 | margin-left: 50px; 51 | z-index: 2; 52 | } 53 | 54 | .bar { 55 | position:relative; 56 | display:block; 57 | width:100%; 58 | } 59 | 60 | .bar:before, .bar:after { 61 | content: ''; 62 | height: 2px; 63 | width: 0; 64 | bottom: 1px; 65 | position: absolute; 66 | background: $green; 67 | transition: 0.2s ease all; 68 | } 69 | 70 | .bar:before { 71 | left: 50%; 72 | } 73 | 74 | .bar:after { 75 | right: 50%; 76 | } 77 | 78 | /* active state */ 79 | input:focus ~ .bar:before, input:focus ~ .bar:after { 80 | width: 50%; 81 | } 82 | 83 | .form-control { 84 | font-size: 16px; 85 | padding: 10px 0px 10px 20px; 86 | display: block; 87 | width: 85%; 88 | border: none; 89 | transition: border 0.2s ease; 90 | background-color: transparent; 91 | } 92 | } 93 | 94 | .helper-text { 95 | position: absolute; 96 | padding: 5px 0px 0px 20px; 97 | font-size: 12px; 98 | color: $gray-600; 99 | top: 100%; 100 | } 101 | 102 | .form-group { 103 | position: relative; 104 | margin-bottom: 0; 105 | .form-control { 106 | @extend .input-group; 107 | @extend .form-control; 108 | } 109 | } 110 | 111 | .input-group-append { 112 | position: absolute; 113 | top: 18px; 114 | right: 30px; 115 | color: #757575; 116 | font-size: 18px; 117 | transition: all 0.2s ease; 118 | } 119 | 120 | .input-group-prepend { 121 | @extend .input-group-append; 122 | right: 0; 123 | left: 20px; 124 | } 125 | 126 | .has-error { 127 | .input-group-prepend, 128 | .input-group-append, 129 | .input-label, 130 | .helper-text { 131 | color: $red !important; 132 | } 133 | 134 | .bar:before, 135 | .bar:after { 136 | background-color: $red; 137 | } 138 | } -------------------------------------------------------------------------------- /src/components/auth/login/login.component.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @author TeaInside 3 | * @version 0.0.1 4 | * @license MIT 5 | */ 6 | 7 | import { Component } from 'Modules/core'; 8 | import { Router } from 'Modules/router'; 9 | 10 | import { AuthService } from 'Services/auth.service'; 11 | import { NotificationService } from 'Services/notification.service'; 12 | 13 | @Component({ 14 | 'selector': 'app-login', 15 | 'template': require('./login.component.html'), 16 | 'styles': [require('./login.component.scss')] 17 | }) 18 | export class LoginComponent { 19 | constructor() { 20 | this.authService = new AuthService(); 21 | this.notifService = new NotificationService(); 22 | } 23 | 24 | onInit() { 25 | 26 | } 27 | 28 | onAfterView() { 29 | this.submitListener = document.getElementById('login-form'); 30 | this.submitListener.addEventListener('submit', (event) => { 31 | event.preventDefault(); 32 | this.login(); 33 | this.disableInputs('#login-form'); 34 | }); 35 | 36 | if (this.authService.isLoggedIn() === true) { 37 | Router.navigate('profile'); 38 | } 39 | 40 | this.appendLoginToken(); 41 | } 42 | 43 | onDestroy() { 44 | this.submitListener.removeEventListener('submit', (event) => { 45 | event.preventDefault(); 46 | this.login(); 47 | this.disableInputs('#login-form'); 48 | }); 49 | } 50 | 51 | /* 52 | * @return void 53 | */ 54 | appendLoginToken() { 55 | if (this.authService.isLoggedIn() === true) { 56 | return; 57 | } 58 | 59 | this.authService.getLoginToken() 60 | .then(response => { 61 | if(response.ok === true) { 62 | let loginTokenElement = document.getElementById('login-token'); 63 | if (loginTokenElement !== null) { 64 | loginTokenElement.remove(); 65 | loginTokenElement = undefined; 66 | } 67 | 68 | response.json().then(result => { 69 | let flogin = document.getElementById('login-form'); 70 | let el = document.createElement('input'); 71 | el.id = 'login-token'; 72 | el.type = 'hidden'; 73 | el.name = 'token'; 74 | el.value = result.data.token; 75 | flogin.appendChild(el); 76 | flogin = undefined; 77 | el = undefined; 78 | }); 79 | } else { 80 | this.notifService.showError('Could not request token').show(); 81 | } 82 | }); 83 | } 84 | 85 | disableInputs(selector) { 86 | $(selector).find('input, textarea, button, select').attr('disabled', 'disabled'); 87 | } 88 | 89 | enableInputs(selector) { 90 | $(selector).find('input, textarea, button, select').removeAttr('disabled'); 91 | } 92 | 93 | /* 94 | * @return void 95 | */ 96 | login() { 97 | let formData = document.getElementById('login-form'); 98 | 99 | this.authService.login(formData) 100 | .then(response => { 101 | if(response.ok === true) { 102 | response.json().then(result => { 103 | localStorage.setItem('token_session', result.data.message.token_session); 104 | 105 | this.notifService.showSuccess(result.data.message.state).show(); 106 | this.enableInputs('#login-form'); 107 | Router.navigate('profile'); 108 | // this.appendLoginToken(); 109 | }); 110 | } else { 111 | response.json().then(result => { 112 | this.notifService.showError(result.data.message.state).show(); 113 | this.enableInputs('#login-form'); 114 | this.appendLoginToken(); 115 | }); 116 | } 117 | }) 118 | .catch(error => { 119 | this.notifService.showError('Could not send data to the server. Please try again.').show(); 120 | this.enableInputs('#login-form'); 121 | }); 122 | } 123 | } -------------------------------------------------------------------------------- /src/components/auth/register/register.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |
5 | 6 | 7 | 8 |
9 | 10 |
11 | Required 12 |
13 |
14 |
15 |
16 | 17 | 18 | 19 |
20 | 21 |
22 | Required 23 |
24 |
25 |
26 | 27 |
28 | 29 | 30 | 31 |
32 | 33 |
34 | Required 35 |
36 | 37 |
38 | 39 | 40 | 41 |
42 | 43 |
44 | Required 45 |
46 | 47 |
48 | 49 | 50 | 51 |
52 | 53 |
54 | Required 55 |
56 | 57 |
58 | 59 | 60 | 61 |
62 | 63 |
64 | Required 65 |
66 | 67 |
68 | 73 | Required 74 |
75 | 76 |
77 |
78 |
79 | Loading... 80 |
81 |
82 |
83 | 84 | 85 | 86 |
87 |
88 | 89 | 90 |

Have an account?

91 | Sign in 92 |
-------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const webpack = require('webpack'); 2 | const path = require('path'); 3 | const HtmlWebpackPlugin = require('html-webpack-plugin'); 4 | const MiniCssExtractPlugin = require('mini-css-extract-plugin'); 5 | const CopyWebpackPlugin = require('copy-webpack-plugin'); 6 | const CleanWebpackPlugin = require('clean-webpack-plugin'); 7 | const devMode = process.env.NODE_ENV !== 'production'; 8 | 9 | module.exports = { 10 | entry: { 11 | styles: './src/scss/styles.scss', 12 | custom: './src/scss/custom.scss', 13 | app: './src/app.js' 14 | }, 15 | resolve: { 16 | alias: { 17 | Modules: path.resolve(__dirname, 'src/modules'), 18 | Components: path.resolve(__dirname, 'src/components'), 19 | Services: path.resolve(__dirname, 'src/services') 20 | } 21 | }, 22 | output: { 23 | filename: 'assets/js/[name].[contenthash].js', 24 | chunkFilename: 'assets/js/[name].[contenthash].js', 25 | sourceMapFilename: "assets/js/[name].[contenthash].map", 26 | path: path.join(__dirname, 'public'), 27 | publicPath: '/' 28 | }, 29 | node: { 30 | __dirname: true 31 | }, 32 | devtool: 'eval', 33 | devServer: { 34 | port: 3000, 35 | contentBase: path.join(__dirname, 'src/assets'), 36 | publicPath: '/', 37 | historyApiFallback: true, 38 | watchContentBase: true 39 | }, 40 | plugins: [ 41 | new webpack.ProvidePlugin({ 42 | $: 'jquery', 43 | jQuery: 'jquery' 44 | }), 45 | new HtmlWebpackPlugin({ 46 | template: './src/index.html' 47 | }), 48 | new MiniCssExtractPlugin({ 49 | filename: "assets/css/[name].[contenthash].css", 50 | chunkFilename: "assets/css/[name].[contenthash].css" 51 | }), 52 | new CleanWebpackPlugin(), 53 | new CopyWebpackPlugin( 54 | [{ 55 | from: './src/assets', 56 | to: 'assets' 57 | }, 58 | { 59 | from: './src/manifest.json', 60 | to: 'manifest.json' 61 | }, 62 | { 63 | from: './node_modules/@fortawesome/fontawesome-free/css', 64 | to: 'assets/vendors/fontawesome/css' 65 | }, 66 | { 67 | from: './node_modules/@fortawesome/fontawesome-free/webfonts', 68 | to: 'assets/vendors/fontawesome/webfonts' 69 | }, 70 | { 71 | from: './node_modules/animate.css/animate.min.css', 72 | to: 'assets/vendors/animate.css/animate.min.css' 73 | }, 74 | { 75 | from: './node_modules/noty/lib', 76 | to: 'assets/vendors/noty' 77 | }], 78 | { 79 | debug: false 80 | }) 81 | ], 82 | module: { 83 | rules: [ 84 | { 85 | test: /\.(sa|sc|c)ss$/, 86 | use: [ 87 | devMode ? 'style-loader' : MiniCssExtractPlugin.loader , 88 | { 89 | loader: 'css-loader', 90 | options: { 91 | sourceMap: false 92 | } 93 | }, 94 | { 95 | loader: 'postcss-loader', 96 | options: { 97 | ident: 'postcss', 98 | plugins: [ 99 | require('autoprefixer') 100 | ] 101 | } 102 | }, 103 | { 104 | loader: 'sass-loader', 105 | options: { 106 | implementation: require('sass') 107 | } 108 | } 109 | ] 110 | }, 111 | { 112 | test: /\.html$/, 113 | loader: 'html-loader' 114 | }, 115 | { 116 | test: /\.js$/, 117 | exclude: /node_modules/, 118 | use: [ 119 | 'babel-loader', 120 | 'eslint-loader' 121 | ] 122 | }, 123 | { 124 | test: /\.(woff(2)?|ttf|eot|svg)$/, 125 | loader: 'file-loader', 126 | options: { 127 | name: '[name].[ext]', 128 | outputPath: 'assets/webfonts/' 129 | } 130 | }, 131 | { 132 | test: /\.(png|jp(e)?g|gif)$/, 133 | loader: 'file-loader', 134 | options: { 135 | outputPath: 'assets/images/[name].[ext]' 136 | } 137 | }, 138 | { 139 | test: /\.(txt|json)$/, 140 | use: 'raw-loader' 141 | }, 142 | ] 143 | } 144 | }; -------------------------------------------------------------------------------- /src/services/auth.service.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @author TeaInside 3 | * @version 0.0.1 4 | * @license MIT 5 | */ 6 | 7 | import { Configs } from '../config.js'; 8 | 9 | export class AuthService { 10 | constructor() { 11 | this.apiUrl = new URL(Configs.apiUrl); 12 | this.apiEp = Configs.apiEndpoints; 13 | } 14 | 15 | /* 16 | * @return Promise 17 | */ 18 | getLoginToken() { 19 | let params = new URLSearchParams(this.apiUrl); 20 | params.append('action', 'get_token'); 21 | 22 | this.apiUrl.pathname = this.apiEp.login; 23 | this.apiUrl.search = params; 24 | let request = new Request(this.apiUrl.href, { 25 | method: 'GET', 26 | mode: 'cors' 27 | }); 28 | 29 | return fetch(request); 30 | } 31 | 32 | /* 33 | * @return Promise 34 | */ 35 | getRegisterToken() { 36 | let params = new URLSearchParams(this.apiUrl); 37 | params.append('action', 'get_token'); 38 | 39 | this.apiUrl.pathname = this.apiEp.register; 40 | this.apiUrl.search = params; 41 | let request = new Request(this.apiUrl.href, { 42 | method: 'GET', 43 | mode: 'cors' 44 | }); 45 | 46 | return fetch(request); 47 | } 48 | 49 | /** 50 | * @param token string 51 | * 52 | * @return Promise 53 | */ 54 | getCaptcha(token) { 55 | let params = new URLSearchParams(this.apiUrl); 56 | params.append('token', token); 57 | 58 | this.apiUrl.pathname = this.apiEp.captcha; 59 | this.apiUrl.search = params; 60 | let request = new Request(this.apiUrl.href, { 61 | method: 'GET', 62 | mode: 'cors' 63 | }); 64 | 65 | return fetch(request); 66 | } 67 | 68 | /** 69 | * @param formElement HTMLElement 70 | * 71 | * @return Promise 72 | */ 73 | login(formElement) { 74 | let params = new URLSearchParams(this.apiUrl); 75 | params.append('action', 'login'); 76 | this.apiUrl.pathname = this.apiEp.login; 77 | this.apiUrl.search = params; 78 | 79 | let fd = new FormData(formElement); 80 | let data = objectFromFormData(fd); 81 | let request = new Request(this.apiUrl.href, { 82 | method: 'POST', 83 | mode: 'cors', 84 | cache: "no-cache", 85 | credentials: "same-origin", 86 | headers: { 87 | "Content-Type": "application/json", 88 | "Authorization": `Bearer ${data.token}` 89 | }, 90 | body: JSON.stringify(data) 91 | }); 92 | 93 | return fetch(request); 94 | } 95 | 96 | /** 97 | * @param formElement HTMLElement 98 | * 99 | * @return Promise 100 | */ 101 | register(formElement) { 102 | let params = new URLSearchParams(this.apiUrl); 103 | params.append('action', 'submit'); 104 | this.apiUrl.pathname = this.apiEp.register; 105 | this.apiUrl.search = params; 106 | 107 | let fd = new FormData(formElement); 108 | let data = objectFromFormData(fd); 109 | let request = new Request(this.apiUrl.href, { 110 | method: 'POST', 111 | mode: 'cors', 112 | cache: "no-cache", 113 | credentials: "same-origin", 114 | headers: { 115 | "Content-Type": "application/json", 116 | "Authorization": `Bearer ${data.token}` 117 | }, 118 | body: JSON.stringify(data) 119 | }); 120 | 121 | return fetch(request); 122 | } 123 | 124 | /** 125 | * @return boolean 126 | */ 127 | isLoggedIn() { 128 | let token = localStorage.getItem('token_session'); 129 | //eslint-disable-next-line no-unused-expressions 130 | return (token !== null) ? true : false; 131 | } 132 | 133 | /** 134 | * @return void 135 | */ 136 | logout() { 137 | localStorage.removeItem('token_session'); 138 | } 139 | } 140 | 141 | /** 142 | * @param formData FormData 143 | * 144 | * @return Object 145 | */ 146 | const objectFromFormData = (formData) => { 147 | let values = {}; 148 | for (let pair of formData.entries()) { 149 | let key = pair[0]; 150 | let value = pair[1]; 151 | if (values[key]) { 152 | if ( ! (values[key] instanceof Array) ) { 153 | values[key] = new Array(values[key]); 154 | } 155 | values[key].push(value); 156 | } else { 157 | values[key] = value; 158 | } 159 | } 160 | return values; 161 | } -------------------------------------------------------------------------------- /src/components/auth/register/register.component.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @author TeaInside 3 | * @version 0.0.1 4 | * @license MIT 5 | */ 6 | 7 | import { Component } from 'Modules/core'; 8 | import { AuthService } from 'Services/auth.service'; 9 | import { NotificationService } from 'Services/notification.service'; 10 | 11 | @Component({ 12 | 'selector': 'app-register', 13 | 'template': require('./register.component.html'), 14 | 'styles': [require('./register.component.scss')] 15 | }) 16 | export class RegisterComponent { 17 | constructor() { 18 | this.authService = new AuthService(); 19 | this.notifService = new NotificationService(); 20 | } 21 | 22 | onInit() { 23 | this.appendRegisterToken(); 24 | } 25 | 26 | onAfterView() { 27 | this.submitListener = document.getElementById('register-form'); 28 | this.submitListener.addEventListener('submit', (event) => { 29 | event.preventDefault(); 30 | this.register(); 31 | this.disableInputs('#register-form'); 32 | }); 33 | } 34 | 35 | onDestroy() { 36 | this.submitListener.removeEventListener('submit', (event) => { 37 | 38 | }); 39 | } 40 | 41 | /* 42 | * @return void 43 | */ 44 | appendRegisterToken() { 45 | this.authService.getRegisterToken() 46 | .then(response => { 47 | if(response.ok === true) { 48 | let loginTokenElement = document.getElementById('register-token'); 49 | if (loginTokenElement !== null) { 50 | loginTokenElement.remove(); 51 | loginTokenElement = undefined; 52 | } 53 | 54 | response.json().then(result => { 55 | let fregister = document.getElementById('register-form'); 56 | let el = document.createElement('input'); 57 | 58 | el.id = 'register-token'; 59 | el.type = 'hidden'; 60 | el.name = 'token'; 61 | el.value = result.data.token; 62 | fregister.appendChild(el); 63 | this.appendCaptcha(result.data.token); 64 | fregister = undefined; 65 | el = undefined; 66 | }); 67 | } else { 68 | this.notifService.showError('Could not request token').show(); 69 | } 70 | }); 71 | } 72 | 73 | /* 74 | * @return void 75 | */ 76 | appendCaptcha(token) { 77 | this.authService.getCaptcha(token) 78 | .then(response => { 79 | if(response.ok === true) { 80 | let captchaImage = document.getElementById('captcha-img'); 81 | let captchaElement = document.getElementById('captcha'); 82 | if (captchaImage !== null) { 83 | captchaImage.remove(); 84 | captchaImage = undefined; 85 | this.toggleSpinner(captchaElement); 86 | } 87 | 88 | response.blob().then(result => { 89 | const base64img = URL.createObjectURL(result); 90 | let captchaElement = document.getElementById('captcha'); 91 | let el = document.createElement('img'); 92 | 93 | el.id = 'captcha-img'; 94 | el.src = base64img; 95 | this.toggleSpinner(captchaElement); 96 | captchaElement.appendChild(el); 97 | }); 98 | } else { 99 | this.notifService.showError('Could not request token').show(); 100 | } 101 | }); 102 | } 103 | 104 | disableInputs(selector) { 105 | $(selector).find('input, textarea, button, select').attr('disabled', 'disabled'); 106 | } 107 | 108 | enableInputs(selector) { 109 | $(selector).find('input, textarea, button, select').removeAttr('disabled'); 110 | } 111 | 112 | register() { 113 | let formData = document.getElementById('register-form'); 114 | this.authService.register(formData) 115 | .then(response => { 116 | if(response.ok === true) { 117 | response.json().then(result => { 118 | this.notifService.showSuccess(result.data.message).show(); 119 | this.enableInputs('#register-form'); 120 | this.appendRegisterToken(); 121 | }); 122 | } else { 123 | response.json().then(result => { 124 | this.notifService.showError(result.data.message).show(); 125 | this.enableInputs('#register-form'); 126 | this.appendRegisterToken(); 127 | }); 128 | } 129 | }) 130 | .catch(error => { 131 | this.notifService.showError('Could not send data to the server. Please try again.').show(); 132 | this.enableInputs('#register-form'); 133 | }); 134 | } 135 | 136 | toggleSpinner(node) { 137 | let spinnerElement = node.children[0]; 138 | spinnerElement.classList.toggle('d-none'); 139 | } 140 | } 141 | -------------------------------------------------------------------------------- /src/assets/css/google-noto-sans.css: -------------------------------------------------------------------------------- 1 | /* noto-sans-regular - vietnamese_latin-ext_devanagari_greek_cyrillic-ext_cyrillic_latin_greek-ext */ 2 | @font-face { 3 | font-family: 'Noto Sans'; 4 | font-style: normal; 5 | font-weight: 400; 6 | src: url('../fonts/noto-sans-v8-vietnamese_latin-ext_devanagari_greek_cyrillic-ext_cyrillic_latin_greek-ext-regular.eot'); /* IE9 Compat Modes */ 7 | src: local('Noto Sans'), local('NotoSans'), 8 | url('../fonts/noto-sans-v8-vietnamese_latin-ext_devanagari_greek_cyrillic-ext_cyrillic_latin_greek-ext-regular.eot?#iefix') format('embedded-opentype'), /* IE6-IE8 */ 9 | url('../fonts/noto-sans-v8-vietnamese_latin-ext_devanagari_greek_cyrillic-ext_cyrillic_latin_greek-ext-regular.woff2') format('woff2'), /* Super Modern Browsers */ 10 | url('../fonts/noto-sans-v8-vietnamese_latin-ext_devanagari_greek_cyrillic-ext_cyrillic_latin_greek-ext-regular.woff') format('woff'), /* Modern Browsers */ 11 | url('../fonts/noto-sans-v8-vietnamese_latin-ext_devanagari_greek_cyrillic-ext_cyrillic_latin_greek-ext-regular.ttf') format('truetype'), /* Safari, Android, iOS */ 12 | url('../fonts/noto-sans-v8-vietnamese_latin-ext_devanagari_greek_cyrillic-ext_cyrillic_latin_greek-ext-regular.svg#NotoSans') format('svg'); /* Legacy iOS */ 13 | } 14 | 15 | /* noto-sans-italic - vietnamese_latin-ext_devanagari_greek_cyrillic-ext_cyrillic_latin_greek-ext */ 16 | @font-face { 17 | font-family: 'Noto Sans'; 18 | font-style: italic; 19 | font-weight: 400; 20 | src: url('../fonts/noto-sans-v8-vietnamese_latin-ext_devanagari_greek_cyrillic-ext_cyrillic_latin_greek-ext-italic.eot'); /* IE9 Compat Modes */ 21 | src: local('Noto Sans Italic'), local('NotoSans-Italic'), 22 | url('../fonts/noto-sans-v8-vietnamese_latin-ext_devanagari_greek_cyrillic-ext_cyrillic_latin_greek-ext-italic.eot?#iefix') format('embedded-opentype'), /* IE6-IE8 */ 23 | url('../fonts/noto-sans-v8-vietnamese_latin-ext_devanagari_greek_cyrillic-ext_cyrillic_latin_greek-ext-italic.woff2') format('woff2'), /* Super Modern Browsers */ 24 | url('../fonts/noto-sans-v8-vietnamese_latin-ext_devanagari_greek_cyrillic-ext_cyrillic_latin_greek-ext-italic.woff') format('woff'), /* Modern Browsers */ 25 | url('../fonts/noto-sans-v8-vietnamese_latin-ext_devanagari_greek_cyrillic-ext_cyrillic_latin_greek-ext-italic.ttf') format('truetype'), /* Safari, Android, iOS */ 26 | url('../fonts/noto-sans-v8-vietnamese_latin-ext_devanagari_greek_cyrillic-ext_cyrillic_latin_greek-ext-italic.svg#NotoSans') format('svg'); /* Legacy iOS */ 27 | } 28 | 29 | /* noto-sans-700 - vietnamese_latin-ext_devanagari_greek_cyrillic-ext_cyrillic_latin_greek-ext */ 30 | @font-face { 31 | font-family: 'Noto Sans'; 32 | font-style: normal; 33 | font-weight: 700; 34 | src: url('../fonts/noto-sans-v8-vietnamese_latin-ext_devanagari_greek_cyrillic-ext_cyrillic_latin_greek-ext-700.eot'); /* IE9 Compat Modes */ 35 | src: local('Noto Sans Bold'), local('NotoSans-Bold'), 36 | url('../fonts/noto-sans-v8-vietnamese_latin-ext_devanagari_greek_cyrillic-ext_cyrillic_latin_greek-ext-700.eot?#iefix') format('embedded-opentype'), /* IE6-IE8 */ 37 | url('../fonts/noto-sans-v8-vietnamese_latin-ext_devanagari_greek_cyrillic-ext_cyrillic_latin_greek-ext-700.woff2') format('woff2'), /* Super Modern Browsers */ 38 | url('../fonts/noto-sans-v8-vietnamese_latin-ext_devanagari_greek_cyrillic-ext_cyrillic_latin_greek-ext-700.woff') format('woff'), /* Modern Browsers */ 39 | url('../fonts/noto-sans-v8-vietnamese_latin-ext_devanagari_greek_cyrillic-ext_cyrillic_latin_greek-ext-700.ttf') format('truetype'), /* Safari, Android, iOS */ 40 | url('../fonts/noto-sans-v8-vietnamese_latin-ext_devanagari_greek_cyrillic-ext_cyrillic_latin_greek-ext-700.svg#NotoSans') format('svg'); /* Legacy iOS */ 41 | } 42 | 43 | /* noto-sans-700italic - vietnamese_latin-ext_devanagari_greek_cyrillic-ext_cyrillic_latin_greek-ext */ 44 | @font-face { 45 | font-family: 'Noto Sans'; 46 | font-style: italic; 47 | font-weight: 700; 48 | src: url('../fonts/noto-sans-v8-vietnamese_latin-ext_devanagari_greek_cyrillic-ext_cyrillic_latin_greek-ext-700italic.eot'); /* IE9 Compat Modes */ 49 | src: local('Noto Sans Bold Italic'), local('NotoSans-BoldItalic'), 50 | url('../fonts/noto-sans-v8-vietnamese_latin-ext_devanagari_greek_cyrillic-ext_cyrillic_latin_greek-ext-700italic.eot?#iefix') format('embedded-opentype'), /* IE6-IE8 */ 51 | url('../fonts/noto-sans-v8-vietnamese_latin-ext_devanagari_greek_cyrillic-ext_cyrillic_latin_greek-ext-700italic.woff2') format('woff2'), /* Super Modern Browsers */ 52 | url('../fonts/noto-sans-v8-vietnamese_latin-ext_devanagari_greek_cyrillic-ext_cyrillic_latin_greek-ext-700italic.woff') format('woff'), /* Modern Browsers */ 53 | url('../fonts/noto-sans-v8-vietnamese_latin-ext_devanagari_greek_cyrillic-ext_cyrillic_latin_greek-ext-700italic.ttf') format('truetype'), /* Safari, Android, iOS */ 54 | url('../fonts/noto-sans-v8-vietnamese_latin-ext_devanagari_greek_cyrillic-ext_cyrillic_latin_greek-ext-700italic.svg#NotoSans') format('svg'); /* Legacy iOS */ 55 | } 56 | 57 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "babel-eslint", 3 | "parserOptions": { 4 | "ecmaVersion": 6, 5 | "sourceType": "module", 6 | "ecmaFeatures": { 7 | "legacyDecorators": true 8 | } 9 | }, 10 | "rules": { 11 | // Enforces getter/setter pairs in objects 12 | "accessor-pairs": 0, 13 | // treat var statements as if they were block scoped 14 | "block-scoped-var": 2, 15 | // specify the maximum cyclomatic complexity allowed in a program 16 | "complexity": [0, 11], 17 | // require return statements to either always or never specify values 18 | "consistent-return": 2, 19 | // specify curly brace conventions for all control statements 20 | "curly": [2, "multi-line"], 21 | // require default case in switch statements 22 | "default-case": 2, 23 | // enforces consistent newlines before or after dots 24 | "dot-location": 0, 25 | // encourages use of dot notation whenever possible 26 | "dot-notation": [2, { "allowKeywords": true}], 27 | // require the use of === and !== 28 | "eqeqeq": 2, 29 | // make sure for-in loops have an if statement 30 | "guard-for-in": 2, 31 | // disallow the use of alert, confirm, and prompt 32 | "no-alert": 1, 33 | // disallow use of arguments.caller or arguments.callee 34 | "no-caller": 2, 35 | // disallow lexical declarations in case clauses 36 | "no-case-declarations": 2, 37 | // disallow division operators explicitly at beginning of regular expression 38 | "no-div-regex": 0, 39 | // disallow else after a return in an if 40 | "no-else-return": 2, 41 | // disallow use of empty destructuring patterns 42 | "no-empty-pattern": 0, 43 | // disallow comparisons to null without a type-checking operator 44 | "no-eq-null": 0, 45 | // disallow use of eval() 46 | "no-eval": 2, 47 | // disallow adding to native types 48 | "no-extend-native": 2, 49 | // disallow unnecessary function binding 50 | "no-extra-bind": 2, 51 | // disallow fallthrough of case statements 52 | "no-fallthrough": 2, 53 | // disallow the use of leading or trailing decimal points in numeric literals 54 | "no-floating-decimal": 2, 55 | // disallow the type conversions with shorter notations 56 | "no-implicit-coercion": 0, 57 | // disallow use of eval()-like methods 58 | "no-implied-eval": 2, 59 | // disallow this keywords outside of classes or class-like objects 60 | "no-invalid-this": 0, 61 | // disallow usage of __iterator__ property 62 | "no-iterator": 2, 63 | // disallow use of labeled statements 64 | "no-labels": [2, {"allowLoop": true, "allowSwitch": true}], 65 | // disallow unnecessary nested blocks 66 | "no-lone-blocks": 2, 67 | // disallow creation of functions within loops 68 | "no-loop-func": 2, 69 | // disallow the use of magic numbers 70 | "no-magic-numbers": 0, 71 | // disallow use of multiple spaces 72 | "no-multi-spaces": 2, 73 | // disallow use of multiline strings 74 | "no-multi-str": 2, 75 | // disallow reassignments of native objects 76 | "no-native-reassign": 2, 77 | // disallow use of new operator for Function object 78 | "no-new-func": 2, 79 | // disallows creating new instances of String,Number, and Boolean 80 | "no-new-wrappers": 2, 81 | // disallow use of new operator when not part of the assignment or comparison 82 | "no-new": 2, 83 | // disallow use of octal escape sequences in string literals, such as 84 | // var foo = "Copyright \251"; 85 | "no-octal-escape": 2, 86 | // disallow use of (old style) octal literals 87 | "no-octal": 2, 88 | // disallow use of process.env 89 | "no-process-env": 0, 90 | // disallow usage of __proto__ property 91 | "no-proto": 2, 92 | // disallow declaring the same variable more then once 93 | "no-redeclare": 2, 94 | // disallow use of assignment in return statement 95 | "no-return-assign": 2, 96 | // disallow use of `javascript:` urls. 97 | "no-script-url": 2, 98 | // disallow comparisons where both sides are exactly the same 99 | "no-self-compare": 2, 100 | // disallow use of comma operator 101 | "no-sequences": 2, 102 | // restrict what can be thrown as an exception 103 | "no-throw-literal": 2, 104 | // disallow usage of expressions in statement position 105 | "no-unused-expressions": 2, 106 | // disallow unnecessary .call() and .apply() 107 | "no-useless-call": 0, 108 | // disallow unnecessary concatenation of literals or template literals 109 | "no-useless-concat": 0, 110 | // disallow use of void operator 111 | "no-void": 0, 112 | // disallow usage of configurable warning terms in comments: e.g. todo 113 | "no-warning-comments": [0, { "terms": ["todo", "fixme", "xxx"], "location": "start" }], 114 | // disallow use of the with statement 115 | "no-with": 2, 116 | // require use of the second argument for parseInt() 117 | "radix": 2, 118 | // requires to declare all vars on top of their containing scope 119 | "vars-on-top": 2, 120 | // require immediate function invocation to be wrapped in parentheses 121 | "wrap-iife": [2, "outside"], 122 | // require or disallow Yoda conditions 123 | "yoda": 2 124 | }, 125 | "env": { 126 | "browser": true, 127 | "jquery": true, 128 | "es6": true, 129 | "node": true 130 | } 131 | } -------------------------------------------------------------------------------- /src/modules/router/RouterFactory.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @author TeaInside 3 | * @version 0.0.1 4 | * @license MIT 5 | */ 6 | 7 | import path from 'path'; 8 | import lodash from 'lodash'; 9 | import { Render } from '../core'; 10 | import { Router, RouterModule } from './RouterModule'; 11 | 12 | const render = new Render(); 13 | const devMode = process.env.NODE_ENV !== 'production'; 14 | 15 | export class RouterFactory { 16 | constructor() { 17 | this.routes = {}; 18 | } 19 | 20 | /** 21 | * Register route path with given routes object 22 | * and return the resolved route object 23 | * 24 | * @param routes Object 25 | * @return Object 26 | */ 27 | registerRoute(routes) { 28 | const router = Router; 29 | const instance = {}; 30 | const routePath = path.resolve(routes.path); 31 | const hasParent = (typeof routes.parent !== 'undefined'); 32 | const hasChildren = (typeof routes.children !== 'undefined'); 33 | 34 | router.on( 35 | routePath, 36 | (params, query) => { 37 | if (hasParent) { 38 | loadParent(routes); 39 | } 40 | 41 | if (typeof routes.redirectTo === 'undefined') { 42 | callOnInit(instance.routes); 43 | render.appendToDOM(routes.component.selector, routes.component.template); 44 | } 45 | 46 | router.updatePageLinks(); 47 | }, 48 | { 49 | before: (done, params) => { 50 | if (typeof routes.redirectTo !== 'undefined') { 51 | Router.navigate(routes.redirectTo); 52 | done(false); 53 | } 54 | 55 | if (typeof routes.component !== 'undefined') { 56 | instance.routes = new routes.component(); 57 | } 58 | 59 | done(true); 60 | }, 61 | after: (params) => { 62 | // callOnAfterView(instance.parent); 63 | callOnAfterView(instance.routes); 64 | }, 65 | leave: (params) => { 66 | callOnDestroy(instance.routes); 67 | render.clearElement(routes.component.selector); 68 | 69 | if (hasParent) { 70 | unloadParent(routes); 71 | } 72 | } 73 | } 74 | ).resolve(); 75 | 76 | if (hasChildren) { 77 | registerChildren(routes); 78 | return router.lastRouteResolved(); 79 | } 80 | 81 | return router.lastRouteResolved(); 82 | } 83 | } 84 | 85 | /** 86 | * Register route path with given routes object 87 | * if the route has a children routes 88 | * 89 | * @param routes Object 90 | * @return Object 91 | */ 92 | function registerChildren(routes) { 93 | const routerFactory = new RouterFactory(); 94 | let parent = routes; 95 | parent.children.map(children => { 96 | let routePath = ''; 97 | if (children.path !== '') { 98 | routePath = path.join(parent.path, children.path); 99 | let route = children; 100 | route.path = routePath; 101 | route.parent = parent; 102 | // route.parent.child = children; 103 | 104 | routerFactory.registerRoute(route); 105 | return route; 106 | } 107 | return children; 108 | }); 109 | parent = undefined; 110 | return routes; 111 | } 112 | 113 | /** 114 | * Recursively render view and call onInit and onAfterView event 115 | * if the current route has a parent. 116 | * 117 | * @param routes Object 118 | * @return boolean|Object 119 | */ 120 | function loadParent(routes) { 121 | const hasParent = _.has(routes, 'parent'); 122 | if (hasParent) { 123 | let grandParent = loadParent(routes.parent); 124 | 125 | if (typeof grandParent !== 'undefined') { 126 | let parentMeta = routes.parent.component; 127 | let instance = new routes.parent.component(); 128 | callOnInit(instance); 129 | render.appendToDOM(parentMeta.selector, parentMeta.template); 130 | callOnAfterView(instance); 131 | 132 | parentMeta = undefined; 133 | instance = undefined; 134 | grandParent = undefined; 135 | return routes.parent; 136 | } 137 | return loadParent(grandParent); 138 | } 139 | return hasParent; 140 | } 141 | 142 | /** 143 | * Recursively remove view and call onDestroy event 144 | * until the current route has no more parent/grand parent/etc 145 | * 146 | * @param routes Object 147 | * @return boolean 148 | */ 149 | function unloadParent(routes) { 150 | const hasParent = _.has(routes, 'parent'); 151 | if (hasParent) { 152 | let parentMeta = routes.parent.component; 153 | let instance = routes.parent.component.prototype; 154 | callOnDestroy(instance); 155 | render.clearElement(parentMeta.selector); 156 | 157 | parentMeta = undefined; 158 | instance = undefined; 159 | return unloadParent(routes.parent); 160 | } 161 | return hasParent; 162 | } 163 | 164 | /** 165 | * Call onInit event 166 | * 167 | * @param component Instance 168 | * @return void 169 | */ 170 | function callOnInit(component) { 171 | try { 172 | if (!_.isUndefined(component.onInit())) { 173 | component.onInit(); 174 | } 175 | } catch(e) { 176 | if (devMode) { 177 | console.log(e); 178 | } 179 | } 180 | } 181 | 182 | /** 183 | * Call onAfterView event 184 | * 185 | * @param component Instance 186 | * @return void 187 | */ 188 | function callOnAfterView(component) { 189 | try { 190 | if (!_.isUndefined(component.onAfterView())) { 191 | component.onAfterView(); 192 | } 193 | } catch(e) { 194 | if (devMode) { 195 | console.log(e); 196 | } 197 | } 198 | } 199 | 200 | /** 201 | * Call onDestroy event 202 | * 203 | * @param component Instance 204 | * @return void 205 | */ 206 | function callOnDestroy(component) { 207 | try { 208 | if (!_.isUndefined(component.onDestroy())) { 209 | component.onDestroy(); 210 | } 211 | } catch(e) { 212 | if (devMode) { 213 | console.log(e); 214 | } 215 | } 216 | } -------------------------------------------------------------------------------- /src/scss/_variables.scss: -------------------------------------------------------------------------------- 1 | // Variables 2 | // 3 | // Variables should follow the `$component-state-property-size` formula for 4 | // consistent naming. Ex: $nav-link-disabled-color and $modal-content-box-shadow-xs. 5 | 6 | // Color system 7 | 8 | $white: #fff ; 9 | $gray-100: #f8f9fa ; 10 | $gray-200: #e9ecef ; 11 | $gray-300: #dee2e6 ; 12 | $gray-400: #ced4da ; 13 | $gray-500: #adb5bd ; 14 | $gray-600: #6c757d ; 15 | $gray-700: #495057 ; 16 | $gray-800: #343a40 ; 17 | $gray-900: #212529 ; 18 | $black: #000 ; 19 | 20 | $grays: () ; 21 | // stylelint-disable-next-line scss/dollar-variable-default 22 | $grays: map-merge( 23 | ( 24 | "100": $gray-100, 25 | "200": $gray-200, 26 | "300": $gray-300, 27 | "400": $gray-400, 28 | "500": $gray-500, 29 | "600": $gray-600, 30 | "700": $gray-700, 31 | "800": $gray-800, 32 | "900": $gray-900 33 | ), 34 | $grays 35 | ); 36 | 37 | $blue: #3498db ; 38 | $indigo: #7c97ee ; 39 | $purple: #8e44ad ; 40 | $pink: #f4a4b1 ; 41 | $red: #c0392b ; 42 | $orange: #e67e22 ; 43 | $yellow: #f1c40f ; 44 | $green: #27ae60 ; 45 | $teal: ‎#008080 ; 46 | $cyan: #17a2b8 ; 47 | 48 | $colors: () ; 49 | // stylelint-disable-next-line scss/dollar-variable-default 50 | $colors: map-merge( 51 | ( 52 | "blue": $blue, 53 | "indigo": $indigo, 54 | "purple": $purple, 55 | "pink": $pink, 56 | "red": $red, 57 | "orange": $orange, 58 | "yellow": $yellow, 59 | "green": $green, 60 | "teal": $teal, 61 | "cyan": $cyan, 62 | "white": $white, 63 | "gray": $gray-600, 64 | "gray-dark": $gray-800 65 | ), 66 | $colors 67 | ); 68 | 69 | $primary: $green ; 70 | $secondary: $gray-400 ; 71 | $success: $green ; 72 | $info: $blue ; 73 | $warning: $yellow ; 74 | $danger: $red ; 75 | $light: $gray-100 ; 76 | $dark: $gray-800 ; 77 | 78 | $theme-colors: () ; 79 | // stylelint-disable-next-line scss/dollar-variable-default 80 | $theme-colors: map-merge( 81 | ( 82 | "primary": $primary, 83 | "secondary": $secondary, 84 | "success": $success, 85 | "info": $info, 86 | "warning": $warning, 87 | "danger": $danger, 88 | "light": $light, 89 | "dark": $dark 90 | ), 91 | $theme-colors 92 | ); 93 | 94 | // Set a specific jump point for requesting color jumps 95 | $theme-color-interval: 8% ; 96 | 97 | // The yiq lightness value that determines when the lightness of color changes from "dark" to "light". Acceptable values are between 0 and 255. 98 | $yiq-contrasted-threshold: 150 ; 99 | 100 | // Customize the light and dark text colors for use in our YIQ color contrast function. 101 | $yiq-text-dark: $gray-900 ; 102 | $yiq-text-light: $white ; 103 | 104 | 105 | // Options 106 | // 107 | // Quickly modify global styling by enabling or disabling optional features. 108 | 109 | $enable-caret: true ; 110 | $enable-rounded: true ; 111 | $enable-shadows: false ; 112 | $enable-gradients: false ; 113 | $enable-transitions: true ; 114 | $enable-prefers-reduced-motion-media-query: true ; 115 | $enable-hover-media-query: false ; // Deprecated, no longer affects any compiled CSS 116 | $enable-grid-classes: true ; 117 | $enable-print-styles: true ; 118 | $enable-validation-icons: true ; 119 | 120 | 121 | // Spacing 122 | // 123 | // Control the default styling of most Bootstrap elements by modifying these 124 | // variables. Mostly focused on spacing. 125 | // You can add more entries to the $spacers map, should you need more variation. 126 | 127 | $spacer: 1rem ; 128 | $spacers: () ; 129 | // stylelint-disable-next-line scss/dollar-variable-default 130 | $spacers: map-merge( 131 | ( 132 | 0: 0, 133 | 1: ($spacer * .25), 134 | 2: ($spacer * .5), 135 | 3: $spacer, 136 | 4: ($spacer * 1.5), 137 | 5: ($spacer * 3) 138 | ), 139 | $spacers 140 | ); 141 | 142 | // This variable affects the `.h-*` and `.w-*` classes. 143 | $sizes: () ; 144 | // stylelint-disable-next-line scss/dollar-variable-default 145 | $sizes: map-merge( 146 | ( 147 | 25: 25%, 148 | 50: 50%, 149 | 75: 75%, 150 | 100: 100%, 151 | auto: auto 152 | ), 153 | $sizes 154 | ); 155 | 156 | 157 | // Body 158 | // 159 | // Settings for the `` element. 160 | 161 | $body-bg: $gray-100 ; 162 | $body-color: $gray-900 ; 163 | 164 | 165 | // Links 166 | // 167 | // Style anchor elements. 168 | 169 | $link-color: theme-color("primary") ; 170 | $link-decoration: none ; 171 | $link-hover-color: darken($link-color, 15%) ; 172 | $link-hover-decoration: underline ; 173 | // Darken percentage for links with `.text-*` class (e.g. `.text-success`) 174 | $emphasized-link-hover-darken-percentage: 15% ; 175 | 176 | // Paragraphs 177 | // 178 | // Style p element. 179 | 180 | $paragraph-margin-bottom: 1rem ; 181 | 182 | 183 | // Grid breakpoints 184 | // 185 | // Define the minimum dimensions at which your layout will change, 186 | // adapting to different screen sizes, for use in media queries. 187 | 188 | $grid-breakpoints: () ; 189 | // stylelint-disable-next-line scss/dollar-variable-default 190 | $grid-breakpoints: map-merge( 191 | ( 192 | xs: 0, 193 | sm: 576px, 194 | md: 768px, 195 | lg: 992px, 196 | xl: 1200px 197 | ), 198 | $grid-breakpoints 199 | ); 200 | 201 | @include _assert-ascending($grid-breakpoints, "$grid-breakpoints"); 202 | @include _assert-starts-at-zero($grid-breakpoints); 203 | 204 | 205 | // Grid containers 206 | // 207 | // Define the maximum width of `.container` for different screen sizes. 208 | 209 | $container-max-widths: () ; 210 | // stylelint-disable-next-line scss/dollar-variable-default 211 | $container-max-widths: map-merge( 212 | ( 213 | sm: 540px, 214 | md: 720px, 215 | lg: 960px, 216 | xl: 1140px 217 | ), 218 | $container-max-widths 219 | ); 220 | 221 | @include _assert-ascending($container-max-widths, "$container-max-widths"); 222 | 223 | 224 | // Grid columns 225 | // 226 | // Set the number of columns and specify the width of the gutters. 227 | 228 | $grid-columns: 12 ; 229 | $grid-gutter-width: 30px ; 230 | 231 | 232 | // Components 233 | // 234 | // Define common padding and border radius sizes and more. 235 | 236 | $line-height-lg: 1.5 ; 237 | $line-height-sm: 1.5 ; 238 | 239 | $border-width: 1px ; 240 | $border-color: $gray-300 ; 241 | 242 | $border-radius: .25rem ; 243 | $border-radius-lg: .3rem ; 244 | $border-radius-sm: .2rem ; 245 | 246 | $rounded-pill: 50rem ; 247 | 248 | $box-shadow-sm: 0 .125rem .25rem rgba($black, .075) ; 249 | $box-shadow: 0 .5rem 1rem rgba($black, .15) ; 250 | $box-shadow-lg: 0 1rem 3rem rgba($black, .175) ; 251 | 252 | $component-active-color: $white ; 253 | $component-active-bg: theme-color("primary") ; 254 | 255 | $caret-width: .3em ; 256 | 257 | $transition-base: all .2s ease-in-out ; 258 | $transition-fade: opacity .15s linear ; 259 | $transition-collapse: height .35s ease ; 260 | 261 | $embed-responsive-aspect-ratios: () ; 262 | // stylelint-disable-next-line scss/dollar-variable-default 263 | $embed-responsive-aspect-ratios: join( 264 | ( 265 | (21 9), 266 | (16 9), 267 | (3 4), 268 | (1 1), 269 | ), 270 | $embed-responsive-aspect-ratios 271 | ); 272 | 273 | // Fonts 274 | // 275 | // Font, line-height, and color for body text, headings, and more. 276 | 277 | // stylelint-disable value-keyword-case 278 | $font-family-sans-serif: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji" ; 279 | $font-family-monospace: SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace ; 280 | $font-family-base: $font-family-sans-serif ; 281 | // stylelint-enable value-keyword-case 282 | 283 | $font-size-base: 1rem ; // Assumes the browser default, typically `16px` 284 | $font-size-lg: ($font-size-base * 1.25) ; 285 | $font-size-sm: ($font-size-base * .875) ; 286 | 287 | $font-weight-lighter: lighter ; 288 | $font-weight-light: 300 ; 289 | $font-weight-normal: 400 ; 290 | $font-weight-bold: 700 ; 291 | $font-weight-bolder: bolder ; 292 | 293 | $font-weight-base: $font-weight-normal ; 294 | $line-height-base: 1.5 ; 295 | 296 | $h1-font-size: $font-size-base * 2.5 ; 297 | $h2-font-size: $font-size-base * 2 ; 298 | $h3-font-size: $font-size-base * 1.75 ; 299 | $h4-font-size: $font-size-base * 1.5 ; 300 | $h5-font-size: $font-size-base * 1.25 ; 301 | $h6-font-size: $font-size-base ; 302 | 303 | $headings-margin-bottom: $spacer / 2 ; 304 | $headings-font-family: inherit ; 305 | $headings-font-weight: 500 ; 306 | $headings-line-height: 1.2 ; 307 | $headings-color: inherit ; 308 | 309 | $display1-size: 6rem ; 310 | $display2-size: 5.5rem ; 311 | $display3-size: 4.5rem ; 312 | $display4-size: 3.5rem ; 313 | 314 | $display1-weight: 300 ; 315 | $display2-weight: 300 ; 316 | $display3-weight: 300 ; 317 | $display4-weight: 300 ; 318 | $display-line-height: $headings-line-height ; 319 | 320 | $lead-font-size: ($font-size-base * 1.25) ; 321 | $lead-font-weight: 300 ; 322 | 323 | $small-font-size: 80% ; 324 | 325 | $text-muted: $gray-600 ; 326 | 327 | $blockquote-small-color: $gray-600 ; 328 | $blockquote-small-font-size: $small-font-size ; 329 | $blockquote-font-size: ($font-size-base * 1.25) ; 330 | 331 | $hr-border-color: rgba($black, .1) ; 332 | $hr-border-width: $border-width ; 333 | 334 | $mark-padding: .2em ; 335 | 336 | $dt-font-weight: $font-weight-bold ; 337 | 338 | $kbd-box-shadow: inset 0 -.1rem 0 rgba($black, .25) ; 339 | $nested-kbd-font-weight: $font-weight-bold ; 340 | 341 | $list-inline-padding: .5rem ; 342 | 343 | $mark-bg: #fcf8e3 ; 344 | 345 | $hr-margin-y: $spacer ; 346 | 347 | 348 | // Tables 349 | // 350 | // Customizes the `.table` component with basic values, each used across all table variations. 351 | 352 | $table-cell-padding: .75rem ; 353 | $table-cell-padding-sm: .3rem ; 354 | 355 | $table-bg: transparent ; 356 | $table-accent-bg: rgba($black, .05) ; 357 | $table-hover-bg: rgba($black, .075) ; 358 | $table-active-bg: $table-hover-bg ; 359 | 360 | $table-border-width: $border-width ; 361 | $table-border-color: $gray-300 ; 362 | 363 | $table-head-bg: $gray-200 ; 364 | $table-head-color: $gray-700 ; 365 | 366 | $table-dark-bg: $gray-900 ; 367 | $table-dark-accent-bg: rgba($white, .05) ; 368 | $table-dark-hover-bg: rgba($white, .075) ; 369 | $table-dark-border-color: lighten($gray-900, 7.5%) ; 370 | $table-dark-color: $white ; 371 | 372 | $table-striped-order: odd ; 373 | 374 | $table-caption-color: $text-muted ; 375 | 376 | $table-bg-level: -9 ; 377 | $table-border-level: -6 ; 378 | 379 | 380 | // Buttons + Forms 381 | // 382 | // Shared variables that are reassigned to `$input-` and `$btn-` specific variables. 383 | 384 | $input-btn-padding-y: .375rem ; 385 | $input-btn-padding-x: .75rem ; 386 | $input-btn-font-size: $font-size-base ; 387 | $input-btn-line-height: $line-height-base ; 388 | 389 | $input-btn-focus-width: .2rem ; 390 | $input-btn-focus-color: rgba($component-active-bg, .25) ; 391 | $input-btn-focus-box-shadow: none; 392 | 393 | $input-btn-padding-y-sm: .25rem ; 394 | $input-btn-padding-x-sm: 1rem ; 395 | $input-btn-font-size-sm: $font-size-sm ; 396 | $input-btn-line-height-sm: $line-height-sm ; 397 | 398 | $input-btn-padding-y-lg: .5rem ; 399 | $input-btn-padding-x-lg: 2rem ; 400 | $input-btn-font-size-lg: $font-size-lg ; 401 | $input-btn-line-height-lg: $line-height-lg ; 402 | 403 | $input-btn-border-width: $border-width ; 404 | 405 | 406 | // Buttons 407 | // 408 | // For each of Bootstrap's buttons, define text, background, and border color. 409 | 410 | $btn-padding-y: $input-btn-padding-y ; 411 | $btn-padding-x: $input-btn-padding-x ; 412 | $btn-font-size: $input-btn-font-size ; 413 | $btn-line-height: $input-btn-line-height ; 414 | 415 | $btn-padding-y-sm: $input-btn-padding-y-sm ; 416 | $btn-padding-x-sm: $input-btn-padding-x-sm ; 417 | $btn-font-size-sm: $input-btn-font-size-sm ; 418 | $btn-line-height-sm: $input-btn-line-height-sm ; 419 | 420 | $btn-padding-y-lg: $input-btn-padding-y-lg ; 421 | $btn-padding-x-lg: $input-btn-padding-x-lg ; 422 | $btn-font-size-lg: $input-btn-font-size-lg ; 423 | $btn-line-height-lg: $input-btn-line-height-lg ; 424 | 425 | $btn-border-width: $input-btn-border-width ; 426 | 427 | $btn-font-weight: $font-weight-normal ; 428 | $btn-box-shadow: inset 0 1px 0 rgba($white, .15), 0 1px 1px rgba($black, .075) ; 429 | $btn-focus-width: $input-btn-focus-width ; 430 | $btn-focus-box-shadow: $input-btn-focus-box-shadow ; 431 | $btn-disabled-opacity: .65 ; 432 | $btn-active-box-shadow: inset 0 3px 5px rgba($black, .125) ; 433 | 434 | $btn-link-disabled-color: $gray-600 ; 435 | 436 | $btn-block-spacing-y: .5rem ; 437 | 438 | // Allows for customizing button radius independently from global border radius 439 | $btn-border-radius: 3rem ; 440 | $btn-border-radius-lg: 3rem ; 441 | $btn-border-radius-sm: 2rem ; 442 | 443 | $btn-transition: color .15s ease-in-out, background-color .15s ease-in-out, border-color .15s ease-in-out, box-shadow .15s ease-in-out ; 444 | 445 | 446 | // Forms 447 | 448 | $label-margin-bottom: .5rem ; 449 | 450 | $input-padding-y: $input-btn-padding-y ; 451 | $input-padding-x: $input-btn-padding-x ; 452 | $input-font-size: $input-btn-font-size ; 453 | $input-font-weight: $font-weight-base ; 454 | $input-line-height: $input-btn-line-height ; 455 | 456 | $input-padding-y-sm: $input-btn-padding-y-sm ; 457 | $input-padding-x-sm: $input-btn-padding-x-sm ; 458 | $input-font-size-sm: $input-btn-font-size-sm ; 459 | $input-line-height-sm: $input-btn-line-height-sm ; 460 | 461 | $input-padding-y-lg: $input-btn-padding-y-lg ; 462 | $input-padding-x-lg: $input-btn-padding-x-lg ; 463 | $input-font-size-lg: $input-btn-font-size-lg ; 464 | $input-line-height-lg: $input-btn-line-height-lg ; 465 | 466 | $input-bg: $white ; 467 | $input-disabled-bg: $gray-200 ; 468 | 469 | $input-color: $gray-700 ; 470 | $input-border-color: $gray-400 ; 471 | $input-border-width: $input-btn-border-width ; 472 | $input-box-shadow: none; 473 | 474 | $input-border-radius: $border-radius ; 475 | $input-border-radius-lg: $border-radius-lg ; 476 | $input-border-radius-sm: $border-radius-sm ; 477 | 478 | $input-focus-bg: $input-bg ; 479 | $input-focus-border-color: $component-active-bg; 480 | $input-focus-color: $input-color ; 481 | $input-focus-width: $input-btn-focus-width ; 482 | $input-focus-box-shadow: $input-btn-focus-box-shadow ; 483 | 484 | $input-placeholder-color: $gray-600 ; 485 | $input-plaintext-color: $body-color ; 486 | 487 | $input-height-border: $input-border-width * 2 ; 488 | 489 | $input-height-inner: ($input-btn-font-size * $input-btn-line-height) + ($input-btn-padding-y * 2) ; 490 | $input-height: calc(#{$input-height-inner} + #{$input-height-border}) ; 491 | 492 | $input-height-inner-sm: ($input-btn-font-size-sm * $input-btn-line-height-sm) + ($input-btn-padding-y-sm * 2) ; 493 | $input-height-sm: calc(#{$input-height-inner-sm} + #{$input-height-border}) ; 494 | 495 | $input-height-inner-lg: ($input-btn-font-size-lg * $input-btn-line-height-lg) + ($input-btn-padding-y-lg * 2) ; 496 | $input-height-lg: calc(#{$input-height-inner-lg} + #{$input-height-border}) ; 497 | 498 | $input-transition: border-color .15s ease-in-out, box-shadow .15s ease-in-out ; 499 | 500 | $form-text-margin-top: .25rem ; 501 | 502 | $form-check-input-gutter: 1.25rem ; 503 | $form-check-input-margin-y: .3rem ; 504 | $form-check-input-margin-x: .25rem ; 505 | 506 | $form-check-inline-margin-x: .75rem ; 507 | $form-check-inline-input-margin-x: .3125rem ; 508 | 509 | $form-grid-gutter-width: 10px ; 510 | $form-group-margin-bottom: 1rem ; 511 | 512 | $input-group-addon-color: $input-color ; 513 | $input-group-addon-bg: transparent ; 514 | $input-group-addon-border-color: $input-border-color ; 515 | 516 | $custom-forms-transition: background-color .15s ease-in-out, border-color .15s ease-in-out, box-shadow .15s ease-in-out ; 517 | 518 | $custom-control-gutter: .5rem ; 519 | $custom-control-spacer-x: 1rem ; 520 | 521 | $custom-control-indicator-size: 1rem ; 522 | $custom-control-indicator-bg: $input-bg ; 523 | 524 | $custom-control-indicator-bg-size: 50% 50% ; 525 | $custom-control-indicator-box-shadow: $input-box-shadow ; 526 | $custom-control-indicator-border-color: $gray-500 ; 527 | $custom-control-indicator-border-width: $input-border-width ; 528 | 529 | $custom-control-indicator-disabled-bg: $input-disabled-bg ; 530 | $custom-control-label-disabled-color: $gray-600 ; 531 | 532 | $custom-control-indicator-checked-color: $component-active-color ; 533 | $custom-control-indicator-checked-bg: $component-active-bg ; 534 | $custom-control-indicator-checked-disabled-bg: rgba(theme-color("primary"), .5) ; 535 | $custom-control-indicator-checked-box-shadow: none ; 536 | $custom-control-indicator-checked-border-color: $custom-control-indicator-checked-bg ; 537 | 538 | $custom-control-indicator-focus-box-shadow: $input-btn-focus-box-shadow ; 539 | $custom-control-indicator-focus-border-color: $input-focus-border-color ; 540 | 541 | $custom-control-indicator-active-color: $component-active-color ; 542 | $custom-control-indicator-active-bg: lighten($component-active-bg, 35%) ; 543 | $custom-control-indicator-active-box-shadow: none ; 544 | $custom-control-indicator-active-border-color: $custom-control-indicator-active-bg ; 545 | 546 | $custom-checkbox-indicator-border-radius: $border-radius ; 547 | $custom-checkbox-indicator-icon-checked: str-replace(url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3e%3cpath fill='#{$custom-control-indicator-checked-color}' d='M6.564.75l-3.59 3.612-1.538-1.55L0 4.26 2.974 7.25 8 2.193z'/%3e%3c/svg%3e"), "#", "%23") ; 548 | 549 | $custom-checkbox-indicator-indeterminate-bg: $component-active-bg ; 550 | $custom-checkbox-indicator-indeterminate-color: $custom-control-indicator-checked-color ; 551 | $custom-checkbox-indicator-icon-indeterminate: str-replace(url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 4 4'%3e%3cpath stroke='#{$custom-checkbox-indicator-indeterminate-color}' d='M0 2h4'/%3e%3c/svg%3e"), "#", "%23") ; 552 | $custom-checkbox-indicator-indeterminate-box-shadow: none ; 553 | $custom-checkbox-indicator-indeterminate-border-color: $custom-checkbox-indicator-indeterminate-bg ; 554 | 555 | $custom-radio-indicator-border-radius: 50% ; 556 | $custom-radio-indicator-icon-checked: str-replace(url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='3' fill='#{$custom-control-indicator-checked-color}'/%3e%3c/svg%3e"), "#", "%23") ; 557 | 558 | $custom-switch-width: $custom-control-indicator-size * 1.75 ; 559 | $custom-switch-indicator-border-radius: $custom-control-indicator-size / 2 ; 560 | $custom-switch-indicator-size: calc(#{$custom-control-indicator-size} - #{$custom-control-indicator-border-width * 4}) ; 561 | 562 | $custom-select-padding-y: $input-btn-padding-y ; 563 | $custom-select-padding-x: $input-btn-padding-x ; 564 | $custom-select-height: $input-height ; 565 | $custom-select-indicator-padding: 1rem ; // Extra padding to account for the presence of the background-image based indicator 566 | $custom-select-font-weight: $input-font-weight ; 567 | $custom-select-line-height: $input-line-height ; 568 | $custom-select-color: $input-color ; 569 | $custom-select-disabled-color: $gray-600 ; 570 | $custom-select-bg: $input-bg ; 571 | $custom-select-disabled-bg: $gray-200 ; 572 | $custom-select-bg-size: 8px 10px ; // In pixels because image dimensions 573 | $custom-select-indicator-color: $gray-800 ; 574 | $custom-select-indicator: str-replace(url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 4 5'%3e%3cpath fill='#{$custom-select-indicator-color}' d='M2 0L0 2h4zm0 5L0 3h4z'/%3e%3c/svg%3e"), "#", "%23") ; 575 | $custom-select-background: $custom-select-indicator no-repeat right $custom-select-padding-x center / $custom-select-bg-size ; // Used so we can have multiple background elements (e.g., arrow and feedback icon) 576 | 577 | $custom-select-feedback-icon-padding-right: $input-height-inner * 3 / 4 + $custom-select-padding-x + $custom-select-indicator-padding ; 578 | $custom-select-feedback-icon-position: center right ($custom-select-padding-x + $custom-select-indicator-padding) ; 579 | $custom-select-feedback-icon-size: ($input-height-inner / 2) ($input-height-inner / 2) ; 580 | 581 | $custom-select-border-width: $input-border-width ; 582 | $custom-select-border-color: $input-border-color ; 583 | $custom-select-border-radius: $border-radius ; 584 | $custom-select-box-shadow: inset 0 1px 2px rgba($black, .075) ; 585 | 586 | $custom-select-focus-border-color: $input-focus-border-color ; 587 | $custom-select-focus-width: $input-focus-width ; 588 | $custom-select-focus-box-shadow: 0 0 0 $custom-select-focus-width rgba($custom-select-focus-border-color, .5) ; 589 | 590 | $custom-select-padding-y-sm: $input-padding-y-sm ; 591 | $custom-select-padding-x-sm: $input-padding-x-sm ; 592 | $custom-select-font-size-sm: $input-btn-font-size-sm ; 593 | $custom-select-height-sm: $input-height-sm ; 594 | 595 | $custom-select-padding-y-lg: $input-padding-y-lg ; 596 | $custom-select-padding-x-lg: $input-padding-x-lg ; 597 | $custom-select-font-size-lg: $input-btn-font-size-lg ; 598 | $custom-select-height-lg: $input-height-lg ; 599 | 600 | $custom-range-track-width: 100% ; 601 | $custom-range-track-height: .5rem ; 602 | $custom-range-track-cursor: pointer ; 603 | $custom-range-track-bg: $gray-300 ; 604 | $custom-range-track-border-radius: 1rem ; 605 | $custom-range-track-box-shadow: inset 0 .25rem .25rem rgba($black, .1) ; 606 | 607 | $custom-range-thumb-width: 1rem ; 608 | $custom-range-thumb-height: $custom-range-thumb-width ; 609 | $custom-range-thumb-bg: $component-active-bg ; 610 | $custom-range-thumb-border: 0 ; 611 | $custom-range-thumb-border-radius: 1rem ; 612 | $custom-range-thumb-box-shadow: 0 .1rem .25rem rgba($black, .1) ; 613 | $custom-range-thumb-focus-box-shadow: 0 0 0 1px $body-bg, $input-focus-box-shadow ; 614 | $custom-range-thumb-focus-box-shadow-width: $input-focus-width ; // For focus box shadow issue in IE/Edge 615 | $custom-range-thumb-active-bg: lighten($component-active-bg, 35%) ; 616 | $custom-range-thumb-disabled-bg: $gray-500 ; 617 | 618 | $custom-file-height: $input-height ; 619 | $custom-file-height-inner: $input-height-inner ; 620 | $custom-file-focus-border-color: $input-focus-border-color ; 621 | $custom-file-focus-box-shadow: $input-focus-box-shadow ; 622 | $custom-file-disabled-bg: $input-disabled-bg ; 623 | 624 | $custom-file-padding-y: $input-padding-y ; 625 | $custom-file-padding-x: $input-padding-x ; 626 | $custom-file-line-height: $input-line-height ; 627 | $custom-file-font-weight: $input-font-weight ; 628 | $custom-file-color: $input-color ; 629 | $custom-file-bg: $input-bg ; 630 | $custom-file-border-width: $input-border-width ; 631 | $custom-file-border-color: $input-border-color ; 632 | $custom-file-border-radius: $input-border-radius ; 633 | $custom-file-box-shadow: $input-box-shadow ; 634 | $custom-file-button-color: $custom-file-color ; 635 | $custom-file-button-bg: $input-group-addon-bg ; 636 | $custom-file-text: ( 637 | en: "Browse" 638 | ) ; 639 | 640 | 641 | // Form validation 642 | 643 | $form-feedback-margin-top: $form-text-margin-top ; 644 | $form-feedback-font-size: $small-font-size ; 645 | $form-feedback-valid-color: theme-color("success") ; 646 | $form-feedback-invalid-color: theme-color("danger") ; 647 | 648 | $form-feedback-icon-valid-color: $form-feedback-valid-color ; 649 | $form-feedback-icon-valid: str-replace(url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3e%3cpath fill='#{$form-feedback-icon-valid-color}' d='M2.3 6.73L.6 4.53c-.4-1.04.46-1.4 1.1-.8l1.1 1.4 3.4-3.8c.6-.63 1.6-.27 1.2.7l-4 4.6c-.43.5-.8.4-1.1.1z'/%3e%3c/svg%3e"), "#", "%23") ; 650 | $form-feedback-icon-invalid-color: $form-feedback-invalid-color ; 651 | $form-feedback-icon-invalid: str-replace(url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' fill='#{$form-feedback-icon-invalid-color}' viewBox='-2 -2 7 7'%3e%3cpath stroke='%23d9534f' d='M0 0l3 3m0-3L0 3'/%3e%3ccircle r='.5'/%3e%3ccircle cx='3' r='.5'/%3e%3ccircle cy='3' r='.5'/%3e%3ccircle cx='3' cy='3' r='.5'/%3e%3c/svg%3E"), "#", "%23") ; 652 | 653 | 654 | // Dropdowns 655 | // 656 | // Dropdown menu container and contents. 657 | 658 | $dropdown-min-width: 10rem ; 659 | $dropdown-padding-y: .5rem ; 660 | $dropdown-spacer: .125rem ; 661 | $dropdown-bg: $white ; 662 | $dropdown-border-color: rgba($black, .15) ; 663 | $dropdown-border-radius: $border-radius ; 664 | $dropdown-border-width: $border-width ; 665 | $dropdown-inner-border-radius: calc(#{$dropdown-border-radius} - #{$dropdown-border-width}) ; 666 | $dropdown-divider-bg: $gray-200 ; 667 | $dropdown-box-shadow: 0 .5rem 1rem rgba($black, .175) ; 668 | 669 | $dropdown-link-color: $gray-900 ; 670 | $dropdown-link-hover-color: darken($gray-900, 5%) ; 671 | $dropdown-link-hover-bg: $gray-100 ; 672 | 673 | $dropdown-link-active-color: $component-active-color ; 674 | $dropdown-link-active-bg: $component-active-bg ; 675 | 676 | $dropdown-link-disabled-color: $gray-600 ; 677 | 678 | $dropdown-item-padding-y: .25rem ; 679 | $dropdown-item-padding-x: 1.5rem ; 680 | 681 | $dropdown-header-color: $gray-600 ; 682 | 683 | 684 | // Z-index master list 685 | // 686 | // Warning: Avoid customizing these values. They're used for a bird's eye view 687 | // of components dependent on the z-axis and are designed to all work together. 688 | 689 | $zindex-dropdown: 1000 ; 690 | $zindex-sticky: 1020 ; 691 | $zindex-fixed: 1030 ; 692 | $zindex-modal-backdrop: 1040 ; 693 | $zindex-modal: 1050 ; 694 | $zindex-popover: 1060 ; 695 | $zindex-tooltip: 1070 ; 696 | 697 | 698 | // Navs 699 | 700 | $nav-link-padding-y: .5rem ; 701 | $nav-link-padding-x: 1rem ; 702 | $nav-link-disabled-color: $gray-600 ; 703 | 704 | $nav-tabs-border-color: $gray-300 ; 705 | $nav-tabs-border-width: $border-width ; 706 | $nav-tabs-border-radius: $border-radius ; 707 | $nav-tabs-link-hover-border-color: $gray-200 $gray-200 $nav-tabs-border-color ; 708 | $nav-tabs-link-active-color: $gray-700 ; 709 | $nav-tabs-link-active-bg: $body-bg ; 710 | $nav-tabs-link-active-border-color: $gray-300 $gray-300 $nav-tabs-link-active-bg ; 711 | 712 | $nav-pills-border-radius: $border-radius ; 713 | $nav-pills-link-active-color: $component-active-color ; 714 | $nav-pills-link-active-bg: $component-active-bg ; 715 | 716 | $nav-divider-color: $gray-200 ; 717 | $nav-divider-margin-y: $spacer / 2 ; 718 | 719 | 720 | // Navbar 721 | 722 | $navbar-padding-y: $spacer / 2 ; 723 | $navbar-padding-x: $spacer ; 724 | 725 | $navbar-nav-link-padding-x: .5rem ; 726 | 727 | $navbar-brand-font-size: $font-size-lg ; 728 | // Compute the navbar-brand padding-y so the navbar-brand will have the same height as navbar-text and nav-link 729 | $nav-link-height: $font-size-base * $line-height-base + $nav-link-padding-y * 2 ; 730 | $navbar-brand-height: $navbar-brand-font-size * $line-height-base ; 731 | $navbar-brand-padding-y: ($nav-link-height - $navbar-brand-height) / 2 ; 732 | 733 | $navbar-toggler-padding-y: .25rem ; 734 | $navbar-toggler-padding-x: .75rem ; 735 | $navbar-toggler-font-size: $font-size-lg ; 736 | $navbar-toggler-border-radius: $btn-border-radius ; 737 | 738 | $navbar-dark-color: rgba($white, .5) ; 739 | $navbar-dark-hover-color: rgba($white, .75) ; 740 | $navbar-dark-active-color: $white ; 741 | $navbar-dark-disabled-color: rgba($white, .25) ; 742 | $navbar-dark-toggler-icon-bg: str-replace(url("data:image/svg+xml,%3csvg viewBox='0 0 30 30' xmlns='http://www.w3.org/2000/svg'%3e%3cpath stroke='#{$navbar-dark-color}' stroke-width='2' stroke-linecap='round' stroke-miterlimit='10' d='M4 7h22M4 15h22M4 23h22'/%3e%3c/svg%3e"), "#", "%23") ; 743 | $navbar-dark-toggler-border-color: rgba($white, .1) ; 744 | 745 | $navbar-light-color: rgba($black, .5) ; 746 | $navbar-light-hover-color: rgba($black, .7) ; 747 | $navbar-light-active-color: rgba($black, .9) ; 748 | $navbar-light-disabled-color: rgba($black, .3) ; 749 | $navbar-light-toggler-icon-bg: str-replace(url("data:image/svg+xml,%3csvg viewBox='0 0 30 30' xmlns='http://www.w3.org/2000/svg'%3e%3cpath stroke='#{$navbar-light-color}' stroke-width='2' stroke-linecap='round' stroke-miterlimit='10' d='M4 7h22M4 15h22M4 23h22'/%3e%3c/svg%3e"), "#", "%23") ; 750 | $navbar-light-toggler-border-color: rgba($black, .1) ; 751 | 752 | $navbar-light-brand-color: $navbar-light-active-color ; 753 | $navbar-light-brand-hover-color: $navbar-light-active-color ; 754 | $navbar-dark-brand-color: $navbar-dark-active-color ; 755 | $navbar-dark-brand-hover-color: $navbar-dark-active-color ; 756 | 757 | 758 | // Pagination 759 | 760 | $pagination-padding-y: .5rem ; 761 | $pagination-padding-x: .75rem ; 762 | $pagination-padding-y-sm: .25rem ; 763 | $pagination-padding-x-sm: .5rem ; 764 | $pagination-padding-y-lg: .75rem ; 765 | $pagination-padding-x-lg: 1.5rem ; 766 | $pagination-line-height: 1.25 ; 767 | 768 | $pagination-color: $link-color ; 769 | $pagination-bg: $white ; 770 | $pagination-border-width: $border-width ; 771 | $pagination-border-color: $gray-300 ; 772 | 773 | $pagination-focus-box-shadow: $input-btn-focus-box-shadow ; 774 | $pagination-focus-outline: 0 ; 775 | 776 | $pagination-hover-color: $link-hover-color ; 777 | $pagination-hover-bg: $gray-200 ; 778 | $pagination-hover-border-color: $gray-300 ; 779 | 780 | $pagination-active-color: $component-active-color ; 781 | $pagination-active-bg: $component-active-bg ; 782 | $pagination-active-border-color: $pagination-active-bg ; 783 | 784 | $pagination-disabled-color: $gray-600 ; 785 | $pagination-disabled-bg: $white ; 786 | $pagination-disabled-border-color: $gray-300 ; 787 | 788 | 789 | // Jumbotron 790 | 791 | $jumbotron-padding: 2rem ; 792 | $jumbotron-bg: $gray-200 ; 793 | 794 | 795 | // Cards 796 | 797 | $card-spacer-y: .75rem ; 798 | $card-spacer-x: 1.25rem ; 799 | $card-border-width: $border-width ; 800 | $card-border-radius: $border-radius ; 801 | $card-border-color: rgba($black, .125) ; 802 | $card-inner-border-radius: calc(#{$card-border-radius} - #{$card-border-width}) ; 803 | $card-cap-bg: rgba($black, .03) ; 804 | $card-cap-color: inherit ; 805 | $card-bg: $white ; 806 | 807 | $card-img-overlay-padding: 1.25rem ; 808 | 809 | $card-group-margin: $grid-gutter-width / 2 ; 810 | $card-deck-margin: $card-group-margin ; 811 | 812 | $card-columns-count: 3 ; 813 | $card-columns-gap: 1.25rem ; 814 | $card-columns-margin: $card-spacer-y ; 815 | 816 | 817 | // Tooltips 818 | 819 | $tooltip-font-size: $font-size-sm ; 820 | $tooltip-max-width: 200px ; 821 | $tooltip-color: $white ; 822 | $tooltip-bg: $black ; 823 | $tooltip-border-radius: $border-radius ; 824 | $tooltip-opacity: .9 ; 825 | $tooltip-padding-y: .25rem ; 826 | $tooltip-padding-x: .5rem ; 827 | $tooltip-margin: 0 ; 828 | 829 | $tooltip-arrow-width: .8rem ; 830 | $tooltip-arrow-height: .4rem ; 831 | $tooltip-arrow-color: $tooltip-bg ; 832 | 833 | // Form tooltips must come after regular tooltips 834 | $form-feedback-tooltip-padding-y: $tooltip-padding-y ; 835 | $form-feedback-tooltip-padding-x: $tooltip-padding-x ; 836 | $form-feedback-tooltip-font-size: $tooltip-font-size ; 837 | $form-feedback-tooltip-line-height: $line-height-base ; 838 | $form-feedback-tooltip-opacity: $tooltip-opacity ; 839 | $form-feedback-tooltip-border-radius: $tooltip-border-radius ; 840 | 841 | 842 | // Popovers 843 | 844 | $popover-font-size: $font-size-sm ; 845 | $popover-bg: $white ; 846 | $popover-max-width: 276px ; 847 | $popover-border-width: $border-width ; 848 | $popover-border-color: rgba($black, .2) ; 849 | $popover-border-radius: $border-radius-lg ; 850 | $popover-box-shadow: 0 .25rem .5rem rgba($black, .2) ; 851 | 852 | $popover-header-bg: darken($popover-bg, 3%) ; 853 | $popover-header-color: $headings-color ; 854 | $popover-header-padding-y: .5rem ; 855 | $popover-header-padding-x: .75rem ; 856 | 857 | $popover-body-color: $body-color ; 858 | $popover-body-padding-y: $popover-header-padding-y ; 859 | $popover-body-padding-x: $popover-header-padding-x ; 860 | 861 | $popover-arrow-width: 1rem ; 862 | $popover-arrow-height: .5rem ; 863 | $popover-arrow-color: $popover-bg ; 864 | 865 | $popover-arrow-outer-color: fade-in($popover-border-color, .05) ; 866 | 867 | 868 | // Toasts 869 | $toast-max-width: 350px ; 870 | $toast-padding-x: .75rem ; 871 | $toast-padding-y: .25rem ; 872 | $toast-font-size: .875rem ; 873 | $toast-background-color: rgba($white, .85) ; 874 | $toast-border-width: 1px ; 875 | $toast-border-color: rgba(0, 0, 0, .1) ; 876 | $toast-border-radius: .25rem ; 877 | $toast-box-shadow: 0 .25rem .75rem rgba($black, .1) ; 878 | 879 | $toast-header-color: $gray-600 ; 880 | $toast-header-background-color: rgba($white, .85) ; 881 | $toast-header-border-color: rgba(0, 0, 0, .05) ; 882 | 883 | 884 | // Badges 885 | 886 | $badge-font-size: 75% ; 887 | $badge-font-weight: $font-weight-bold ; 888 | $badge-padding-y: .25em ; 889 | $badge-padding-x: .4em ; 890 | $badge-border-radius: $border-radius ; 891 | 892 | $badge-pill-padding-x: .6em ; 893 | // Use a higher than normal value to ensure completely rounded edges when 894 | // customizing padding or font-size on labels. 895 | $badge-pill-border-radius: 10rem ; 896 | 897 | 898 | // Modals 899 | 900 | // Padding applied to the modal body 901 | $modal-inner-padding: 1rem ; 902 | 903 | $modal-dialog-margin: .5rem ; 904 | $modal-dialog-margin-y-sm-up: 1.75rem ; 905 | 906 | $modal-title-line-height: $line-height-base ; 907 | 908 | $modal-content-bg: $white ; 909 | $modal-content-border-color: rgba($black, .2) ; 910 | $modal-content-border-width: $border-width ; 911 | $modal-content-border-radius: $border-radius-lg ; 912 | $modal-content-box-shadow-xs: 0 .25rem .5rem rgba($black, .5) ; 913 | $modal-content-box-shadow-sm-up: 0 .5rem 1rem rgba($black, .5) ; 914 | 915 | $modal-backdrop-bg: $black ; 916 | $modal-backdrop-opacity: .5 ; 917 | $modal-header-border-color: $gray-200 ; 918 | $modal-footer-border-color: $modal-header-border-color ; 919 | $modal-header-border-width: $modal-content-border-width ; 920 | $modal-footer-border-width: $modal-header-border-width ; 921 | $modal-header-padding-y: 1rem ; 922 | $modal-header-padding-x: 1rem ; 923 | $modal-header-padding: $modal-header-padding-y $modal-header-padding-x ; // Keep this for backwards compatibility 924 | 925 | $modal-xl: 1140px ; 926 | $modal-lg: 800px ; 927 | $modal-md: 500px ; 928 | $modal-sm: 300px ; 929 | 930 | $modal-fade-transform: translate(0, -50px) ; 931 | $modal-show-transform: none ; 932 | $modal-transition: transform .3s ease-out ; 933 | 934 | 935 | // Alerts 936 | // 937 | // Define alert colors, border radius, and padding. 938 | 939 | $alert-padding-y: .75rem ; 940 | $alert-padding-x: 1.25rem ; 941 | $alert-margin-bottom: 1rem ; 942 | $alert-border-radius: $border-radius ; 943 | $alert-link-font-weight: $font-weight-bold ; 944 | $alert-border-width: $border-width ; 945 | 946 | $alert-bg-level: -10 ; 947 | $alert-border-level: -9 ; 948 | $alert-color-level: 6 ; 949 | 950 | 951 | // Progress bars 952 | 953 | $progress-height: 1rem ; 954 | $progress-font-size: ($font-size-base * .75) ; 955 | $progress-bg: $gray-200 ; 956 | $progress-border-radius: $border-radius ; 957 | $progress-box-shadow: inset 0 .1rem .1rem rgba($black, .1) ; 958 | $progress-bar-color: $white ; 959 | $progress-bar-bg: theme-color("primary") ; 960 | $progress-bar-animation-timing: 1s linear infinite ; 961 | $progress-bar-transition: width .6s ease ; 962 | 963 | 964 | // List group 965 | 966 | $list-group-bg: $white ; 967 | $list-group-border-color: rgba($black, .125) ; 968 | $list-group-border-width: $border-width ; 969 | $list-group-border-radius: $border-radius ; 970 | 971 | $list-group-item-padding-y: .75rem ; 972 | $list-group-item-padding-x: 1.25rem ; 973 | 974 | $list-group-hover-bg: $gray-100 ; 975 | $list-group-active-color: $component-active-color ; 976 | $list-group-active-bg: $component-active-bg ; 977 | $list-group-active-border-color: $list-group-active-bg ; 978 | 979 | $list-group-disabled-color: $gray-600 ; 980 | $list-group-disabled-bg: $list-group-bg ; 981 | 982 | $list-group-action-color: $gray-700 ; 983 | $list-group-action-hover-color: $list-group-action-color ; 984 | 985 | $list-group-action-active-color: $body-color ; 986 | $list-group-action-active-bg: $gray-200 ; 987 | 988 | 989 | // Image thumbnails 990 | 991 | $thumbnail-padding: .25rem ; 992 | $thumbnail-bg: $body-bg ; 993 | $thumbnail-border-width: $border-width ; 994 | $thumbnail-border-color: $gray-300 ; 995 | $thumbnail-border-radius: $border-radius ; 996 | $thumbnail-box-shadow: 0 1px 2px rgba($black, .075) ; 997 | 998 | 999 | // Figures 1000 | 1001 | $figure-caption-font-size: 90% ; 1002 | $figure-caption-color: $gray-600 ; 1003 | 1004 | 1005 | // Breadcrumbs 1006 | 1007 | $breadcrumb-padding-y: .75rem ; 1008 | $breadcrumb-padding-x: 1rem ; 1009 | $breadcrumb-item-padding: .5rem ; 1010 | 1011 | $breadcrumb-margin-bottom: 1rem ; 1012 | 1013 | $breadcrumb-bg: $gray-200 ; 1014 | $breadcrumb-divider-color: $gray-600 ; 1015 | $breadcrumb-active-color: $gray-600 ; 1016 | $breadcrumb-divider: quote("/") ; 1017 | 1018 | $breadcrumb-border-radius: $border-radius ; 1019 | 1020 | 1021 | // Carousel 1022 | 1023 | $carousel-control-color: $white ; 1024 | $carousel-control-width: 15% ; 1025 | $carousel-control-opacity: .5 ; 1026 | $carousel-control-hover-opacity: .9 ; 1027 | $carousel-control-transition: opacity .15s ease ; 1028 | 1029 | $carousel-indicator-width: 30px ; 1030 | $carousel-indicator-height: 3px ; 1031 | $carousel-indicator-hit-area-height: 10px ; 1032 | $carousel-indicator-spacer: 3px ; 1033 | $carousel-indicator-active-bg: $white ; 1034 | $carousel-indicator-transition: opacity .6s ease ; 1035 | 1036 | $carousel-caption-width: 70% ; 1037 | $carousel-caption-color: $white ; 1038 | 1039 | $carousel-control-icon-width: 20px ; 1040 | 1041 | $carousel-control-prev-icon-bg: str-replace(url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' fill='#{$carousel-control-color}' viewBox='0 0 8 8'%3e%3cpath d='M5.25 0l-4 4 4 4 1.5-1.5-2.5-2.5 2.5-2.5-1.5-1.5z'/%3e%3c/svg%3e"), "#", "%23") ; 1042 | $carousel-control-next-icon-bg: str-replace(url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' fill='#{$carousel-control-color}' viewBox='0 0 8 8'%3e%3cpath d='M2.75 0l-1.5 1.5 2.5 2.5-2.5 2.5 1.5 1.5 4-4-4-4z'/%3e%3c/svg%3e"), "#", "%23") ; 1043 | 1044 | $carousel-transition-duration: .6s ; 1045 | $carousel-transition: transform $carousel-transition-duration ease-in-out ; // Define transform transition first if using multiple transitions (e.g., `transform 2s ease, opacity .5s ease-out`) 1046 | 1047 | 1048 | // Spinners 1049 | 1050 | $spinner-width: 2rem ; 1051 | $spinner-height: $spinner-width ; 1052 | $spinner-border-width: .25em ; 1053 | 1054 | $spinner-width-sm: 1rem ; 1055 | $spinner-height-sm: $spinner-width-sm ; 1056 | $spinner-border-width-sm: .2em ; 1057 | 1058 | 1059 | // Close 1060 | 1061 | $close-font-size: $font-size-base * 1.5 ; 1062 | $close-font-weight: $font-weight-bold ; 1063 | $close-color: $black ; 1064 | $close-text-shadow: 0 1px 0 $white ; 1065 | 1066 | 1067 | // Code 1068 | 1069 | $code-font-size: 87.5% ; 1070 | $code-color: $pink ; 1071 | 1072 | $kbd-padding-y: .2rem ; 1073 | $kbd-padding-x: .4rem ; 1074 | $kbd-font-size: $code-font-size ; 1075 | $kbd-color: $white ; 1076 | $kbd-bg: $gray-900 ; 1077 | 1078 | $pre-color: $gray-900 ; 1079 | $pre-scrollable-max-height: 340px ; 1080 | 1081 | 1082 | // Utilities 1083 | 1084 | $overflows: auto, hidden ; 1085 | $positions: static, relative, absolute, fixed, sticky ; 1086 | 1087 | 1088 | // Printing 1089 | 1090 | $print-page-size: a3 ; 1091 | $print-body-min-width: map-get($grid-breakpoints, "lg") ; 1092 | -------------------------------------------------------------------------------- /src/assets/fonts/noto-sans-v8-vietnamese_latin-ext_devanagari_greek_cyrillic-ext_cyrillic_latin_greek-ext-700.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 12 | 13 | 14 | 15 | 16 | 18 | 21 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 35 | 36 | 38 | 41 | 42 | 44 | 47 | 48 | 51 | 54 | 56 | 58 | 59 | 60 | 61 | 64 | 67 | 68 | 70 | 71 | 72 | 73 | 74 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 85 | 86 | 88 | 89 | 91 | 92 | 93 | 94 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 107 | 109 | 110 | 112 | 114 | 115 | 117 | 118 | 119 | 120 | 121 | 122 | 124 | 125 | 127 | 129 | 131 | 132 | 134 | 135 | 136 | 137 | 139 | 140 | 141 | 142 | 144 | 145 | 147 | 149 | 150 | 151 | 153 | 154 | 156 | 157 | 158 | 161 | 163 | 166 | 168 | 169 | 170 | 171 | 174 | 175 | 177 | 178 | 179 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 189 | 190 | 192 | 194 | 197 | 199 | 200 | 201 | 203 | 205 | 207 | 209 | 210 | 212 | 213 | 214 | 215 | 217 | 218 | 219 | 220 | 222 | 223 | 225 | 227 | 229 | 231 | 234 | 237 | 238 | 240 | 242 | 244 | 246 | 248 | 249 | 250 | 253 | 255 | 257 | 259 | 262 | 265 | 268 | 271 | 273 | 275 | 277 | 279 | 282 | 283 | 284 | 285 | 287 | 289 | 291 | 293 | 295 | 297 | 300 | 303 | 305 | 307 | 309 | 311 | 313 | 315 | 317 | 319 | 321 | 322 | 323 | 324 | 325 | 326 | 328 | 330 | 331 | 332 | 333 | 334 | 335 | 336 | 337 | --------------------------------------------------------------------------------