├── .babelrc
├── .github
└── winsome.png
├── .gitignore
├── LICENSE
├── README.md
├── index.html
├── package-lock.json
├── package.json
├── src
├── App.vue
├── assets
│ └── winsome-trivia.png
├── components
│ ├── About.vue
│ ├── GameBoard.vue
│ ├── GameOverModal.vue
│ ├── Instructions.vue
│ ├── Intro.vue
│ ├── Loader.vue
│ ├── Questions.vue
│ ├── Score.vue
│ ├── ScoreDots.vue
│ ├── StarPower.vue
│ └── Starter.vue
├── htmlEntityMixin.js
├── main.js
├── main.scss
└── store
│ └── store.js
└── webpack.config.js
/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": [
3 | ["latest", {
4 | "es2015": { "modules": false }
5 | }]
6 | ]
7 | }
8 |
--------------------------------------------------------------------------------
/.github/winsome.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Splode/open-trivia-app/11220dc7c5847bd1a1bd807bb045a30f17a032a1/.github/winsome.png
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | node_modules/
3 | dist/
4 | npm-debug.log
5 | yarn-error.log
6 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2018 Christopher Murphy
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Winsome Trivia
2 |
3 | A single or multiplayer (local) trivia game made with featuring over 2,000 unique questions and built with Vue.js.
4 |
5 |
6 |
7 |
8 |
9 | ## About
10 | A trivia web app built with Vue.js and powered by the [Open Trivia Database](https://opentdb.com/).
11 |
12 | State is managed by [Vuex](https://github.com/vuejs/vuex). Database requests are handled by [vue-resource](https://github.com/pagekit/vue-resource).
13 |
14 | ## License
15 | MIT
16 |
--------------------------------------------------------------------------------
/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 | Winsome Trivia
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "open-trivia-app",
3 | "description": "A single or multiplayer (local) trivia game made with Vue.js .",
4 | "version": "1.0.0",
5 | "author": "Christopher Murphy (christopherianmurphy@gmail.com)",
6 | "license": "MIT",
7 | "private": true,
8 | "scripts": {
9 | "dev": "cross-env NODE_ENV=development webpack-dev-server --open --hot",
10 | "build": "cross-env NODE_ENV=production webpack --progress --hide-modules"
11 | },
12 | "dependencies": {
13 | "vue": "^2.2.1",
14 | "vue-resource": "^1.2.1",
15 | "vuex": "^2.2.1"
16 | },
17 | "devDependencies": {
18 | "babel-core": "^6.0.0",
19 | "babel-loader": "^6.0.0",
20 | "babel-preset-latest": "^6.0.0",
21 | "cross-env": "^3.0.0",
22 | "css-loader": "^0.25.0",
23 | "file-loader": "^0.9.0",
24 | "node-sass": "^4.5.0",
25 | "sass-loader": "^5.0.1",
26 | "vue-loader": "^11.1.4",
27 | "vue-template-compiler": "^2.2.1",
28 | "webpack": "^2.2.0",
29 | "webpack-dev-server": "^2.2.0"
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/src/App.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 | close
10 |
11 |
12 |
13 |
14 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
91 |
92 |
199 |
--------------------------------------------------------------------------------
/src/assets/winsome-trivia.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Splode/open-trivia-app/11220dc7c5847bd1a1bd807bb045a30f17a032a1/src/assets/winsome-trivia.png
--------------------------------------------------------------------------------
/src/components/About.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
triv·i·a
4 |
noun
5 | details, considerations, or pieces of information of little importance or value.
6 |
7 |
I love trivia and I thought that you might too.
8 |
9 |
Powered by the Open Trivia Database .
10 |
11 |
12 |
13 |
17 |
18 |
41 |
--------------------------------------------------------------------------------
/src/components/GameBoard.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
Player One
6 | Player Two
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
34 |
35 |
65 |
--------------------------------------------------------------------------------
/src/components/GameOverModal.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
Game Over!
7 |
Score: {{ scores.playerOne.total }}/10
8 |
{{ scoreMessage }}
9 |
10 |
11 |
12 |
13 |
Player One Wins!
14 | Player Two Wins!
15 | Draw!
16 |
17 | Player One: {{ scores.playerOne.total }}
18 | Player Two: {{ scores.playerTwo.total }}
19 |
20 |
21 |
New Game
22 |
Rematch!
23 |
24 |
25 |
26 |
27 |
71 |
72 |
153 |
--------------------------------------------------------------------------------
/src/components/Instructions.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
Test your trivial knowledge with 10 questions.
4 |
5 | Play against a friend, or challenge yourself.
6 |
7 |
8 |
9 |
12 |
13 |
34 |
--------------------------------------------------------------------------------
/src/components/Intro.vue:
--------------------------------------------------------------------------------
1 |
2 |
6 |
7 |
8 |
18 |
19 |
20 |
--------------------------------------------------------------------------------
/src/components/Loader.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 | {{ message[counter] }}
9 |
10 |
11 |
12 |
13 |
14 |
77 |
78 |
152 |
--------------------------------------------------------------------------------
/src/components/Questions.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
{{ questions[round].category }}
4 |
{{ decodedQuestion }}
5 |
6 |
7 | {{ decode(choice.text) }}
11 |
12 |
13 |
Next
14 |
15 |
16 |
17 |
97 |
98 |
133 |
--------------------------------------------------------------------------------
/src/components/Score.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
Score: {{ scores.playerOne }}/10
4 |
5 |
Player One Turn
6 | Player Two Turn
7 | Player One: {{ scores.playerOne }}
8 | Player Two: {{ scores.playerTwo }}
9 |
10 |
11 |
12 |
13 |
31 |
32 |
56 |
--------------------------------------------------------------------------------
/src/components/ScoreDots.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
12 |
13 |
14 |
15 |
16 |
20 |
21 |
22 |
23 |
27 |
28 |
29 |
30 |
31 |
32 |
54 |
55 |
90 |
--------------------------------------------------------------------------------
/src/components/StarPower.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 | star
7 |
8 |
9 |
10 |
24 |
25 |
92 |
--------------------------------------------------------------------------------
/src/components/Starter.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 | Single Player
7 | Multiplayer
11 | Start
12 |
13 |
14 |
15 |
16 |
17 |
Choose Category
18 |
19 |
20 | expand_more
21 | expand_less
22 |
23 |
24 |
{{ currentCategory.name }}
25 |
26 | Random (default)
27 | {{ category.name }}
28 |
29 |
30 |
31 |
32 |
33 |
90 |
91 |
172 |
--------------------------------------------------------------------------------
/src/htmlEntityMixin.js:
--------------------------------------------------------------------------------
1 | export const htmlEntity = {
2 | methods: {
3 | decode(str) {
4 | return str.replace(/&[#039]*;/g, "'")
5 | .replace(/&[amp]*;/g, '&')
6 | .replace(/&[quot]*;/g, '"')
7 | .replace(/&[rsquo]*;/g, '’')
8 | .replace(/&[lsquo]*;/g, '‘')
9 | .replace(/&[ldquo]*;/g, '“')
10 | .replace(/&[rdquo]*;/g, '”')
11 | .replace(/&[apos]*;/g, "'")
12 | .replace(/&[hellip]*;/g, "…")
13 | .replace(/&[percnt]*;/g, '%')
14 | .replace(/&[divide]*;/g, '÷')
15 | .replace(/&[div]*;/g, '÷')
16 | .replace(/&[lt]*;/g, '<')
17 | .replace(/&[gt]*;/g, '>')
18 | .replace(/&[sup2]*;/g, '²')
19 | .replace(/&[deg]*;/g, '°')
20 | // FIXME: regex matching 'eacute'
21 | // .replace(/&[aacute]*;/g, 'á')
22 | .replace(/&[eacute]*;/g, 'é')
23 | .replace(/&[iacute]*;/g, 'í')
24 | .replace(/&[ntilde]*;/g, 'ñ')
25 | .replace(/&[oacute]*;/g, 'ó')
26 | .replace(/&[uacute]*;/g, 'ú')
27 | .replace(/&[auml]*;/g, 'ä')
28 | .replace(/&[euml]*;/g, 'ë')
29 | .replace(/&[iuml]*;/g, 'ï')
30 | .replace(/&[ouml]*;/g, 'ö')
31 | .replace(/&[uuml]*;/g, 'ü')
32 | .replace(/&[yuml]*;/g, 'ÿ')
33 | .replace(/&[uuml]*;/g, 'ü')
34 | .replace(/&[scaron]*;/g, 'š')
35 | .replace(/&[epsilon]*;/g, 'ε')
36 | .replace(/&[Phi]*;/g, 'φ')
37 | }
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/src/main.js:
--------------------------------------------------------------------------------
1 | import Vue from 'vue'
2 | import VueResource from 'vue-resource';
3 | import App from './App.vue'
4 | import {
5 | store
6 | } from './store/store';
7 |
8 | Vue.use(VueResource);
9 |
10 | new Vue({
11 | el: '#app',
12 | store,
13 | render: h => h(App)
14 | })
15 |
--------------------------------------------------------------------------------
/src/main.scss:
--------------------------------------------------------------------------------
1 | @import url( 'https://fonts.googleapis.com/css?family=Source+Sans+Pro:400,900');
2 | @import url( 'https://fonts.googleapis.com/css?family=Quicksand:400,700');
3 | @import url( 'https://fonts.googleapis.com/icon?family=Material+Icons');
4 | $color-darkest: #0277BD;
5 | $color-dark: #03A9F4;
6 | $color-med: #4FC3F7;
7 | $color-alt-dark: #5C6BC0;
8 | $color-alt-med: #7986CB;
9 | $color-white: whitesmoke;
10 | $color-green: #5CDB80;
11 | $color-red: #FF5593;
12 | $back-gradient: linear-gradient(to top, #4481eb 0%, #04befe 100%);
13 |
14 | body,
15 | ul {
16 | margin: 0;
17 | padding: 0;
18 | }
19 |
20 | a, a.link-light, a.link-dark {
21 | color: inherit;
22 | position: relative;
23 | text-decoration: none;
24 | transition: all .3s ease;
25 |
26 | &::before {
27 | bottom: -2px;
28 | right: 0;
29 | content: '';
30 | position: absolute;
31 | transform: scale(1);
32 | transition: all .3s ease;
33 | width: 100%;
34 | height: 2px;
35 | }
36 |
37 | &:hover::before {
38 | transform: scale(0);
39 | }
40 | }
41 |
42 | a.link-light {
43 | &:hover {
44 | color: $color-med;
45 | }
46 | &::before {
47 | background-color: $color-white;
48 | }
49 | }
50 |
51 | a.link-dark {
52 | &:hover {
53 | color: $color-white;
54 | }
55 | &::before {
56 | background-color: $color-med;
57 | }
58 | }
59 |
60 | button {
61 | align-self: center;
62 | background-color: $color-white;
63 | border: 2px solid $color-white;
64 | //border: 0;
65 | border-radius: 190px;
66 | color: $color-darkest;
67 | cursor: pointer;
68 | font-family: 'Quicksand', sans-serif;
69 | font-size: 1.25em;
70 | margin: .5em 0;
71 | outline: none;
72 | padding: 1em;
73 | transition: all 0.25s ease;
74 | max-width: 400px;
75 | //width: 200px;
76 | width: 100%;
77 | &:disabled, &:disabled:hover {
78 | cursor: auto;
79 | pointer-events: none;
80 | }
81 | }
82 |
83 | h2 {
84 | font-weight: normal;
85 | }
86 |
87 | ul {
88 | list-style-type: none;
89 | }
90 |
91 | @media (hover:hover) {
92 | button:hover, button:active {
93 | background-color: $color-med;
94 | border: 2px solid $color-med;
95 | color: $color-white;
96 | }
97 | }
98 |
99 | @media (max-width: 600px) {
100 | body {
101 | font-size: .85em;
102 | }
103 | }
104 |
105 | .flex-center-col {
106 | align-items: center;
107 | display: flex;
108 | flex-direction: column;
109 | justify-content: center;
110 | }
111 |
112 | .fade-enter-active,
113 | .fade-leave-active {
114 | transition: all 0.3s ease;
115 | }
116 |
117 | .fade-enter,
118 | .fade-leave-to {
119 | opacity: 0;
120 | }
121 |
122 | // Styling for score displays
123 | .score-high {
124 | color: $color-green;
125 | }
126 |
127 | .score-low {
128 | color: $color-red;
129 | }
130 |
131 | @keyframes fade-in {
132 | from {
133 | opacity: 0;
134 | }
135 | to {
136 | opacity: 1;
137 | }
138 | }
139 |
140 | @keyframes open {
141 | from {
142 | max-height: 0;
143 | opacity: 0;
144 | margin-bottom: 0;
145 | }
146 | to {
147 | max-height: 1000px;
148 | opacity: 1;
149 | margin-bottom: 2em;
150 | }
151 | }
152 |
153 | @keyframes close {
154 | from {
155 | max-height: 1000px;
156 | opacity: 1;
157 | margin-bottom: 2em;
158 | }
159 | to {
160 | max-height: 0;
161 | opacity: 0;
162 | margin-bottom: 0;
163 | }
164 | }
165 |
--------------------------------------------------------------------------------
/src/store/store.js:
--------------------------------------------------------------------------------
1 | import Vue from 'vue';
2 | import Vuex from 'vuex';
3 |
4 | Vue.use(Vuex);
5 |
6 | export const store = new Vuex.Store({
7 | state: {
8 | aboutShow: false, // Show or hide About menu
9 | categories: [], // trivia categories
10 | currentCategory: { // chosen category
11 | name: 'Random',
12 | id: 9,
13 | },
14 | currentView: 'app-intro',
15 | questions: [], // current list of questions in game
16 | isGameOver: false, // game state
17 | isPaused: false,
18 | round: 0, // round counter, starts at 0, ends at 9. Linked to display of current question
19 | scores: {
20 | playerOne: {
21 | history: [],
22 | total: 0,
23 | },
24 | playerTwo: {
25 | history: [],
26 | total: 0,
27 | }
28 | },
29 | solo: true, // Game mode, solo or multiplayer?
30 | starPower: false, // show starPower animation
31 | },
32 | // =============== ACTIONS ===============
33 | actions: {
34 | // Call starPower mutation and set delay toggle
35 | starPower(context) {
36 | context.commit('starPower');
37 | let vm = context;
38 | setTimeout(() => {
39 | vm.state.starPower = false;
40 | }, 1600);
41 | },
42 | // Called by Starter.vue.
43 | startGame(context) {
44 | context.state.currentView = 'app-loader';
45 | // Fetch batch of questions for specific category
46 | let api;
47 | // Determine if random (default) or chosen category
48 | if (context.state.currentCategory.name === 'Random') {
49 | api = 'https://opentdb.com/api.php?amount=10';
50 | } else {
51 | api = 'https://opentdb.com/api.php?amount=10&category=' + context.state.currentCategory.id;
52 | }
53 | Vue.http.get(api)
54 | .then(response => {
55 | return response.json();
56 | })
57 | .then(data => {
58 | context.commit('startGame', data.results);
59 | });
60 | },
61 | },
62 | // ========== GETTERS ============
63 | getters: {
64 | // Get solo or multiplayer
65 | mode: state => {
66 | return state.solo;
67 | },
68 | round: state => {
69 | return state.round;
70 | },
71 | // Determine player turn
72 | turn: state => {
73 | let check = state.round % 2;
74 | if (check === 0 || state.round === 0) {
75 | return 'playerOne';
76 | } else {
77 | return 'playerTwo';
78 | }
79 | },
80 | },
81 | // ============ MUTATIONS ===============
82 | mutations: {
83 | // Toggle display of about menu
84 | aboutToggle: state => {
85 | state.aboutShow = !state.aboutShow;
86 | },
87 | // Set game over and show modal after 10 rounds
88 | isGameOver: state => {
89 | if (state.round === 9) {
90 | state.isGameOver = true;
91 | }
92 | },
93 | // Next round
94 | incrementRound: state => {
95 | state.round += 1;
96 | },
97 | // Restart game with default state
98 | newGame: state => {
99 | state.currentCategory.name = 'Random';
100 | state.currentCategory.id = 9;
101 | state.currentView = 'app-intro';
102 | state.solo = true;
103 | },
104 | // Pause game state and disable answer buttons after submtting answer
105 | pauseGame: (state, payload) => {
106 | if (payload === 'pause') {
107 | state.isPaused = true;
108 | } else {
109 | state.isPaused = false;
110 | }
111 | },
112 | // Reset common default game parameters
113 | resetGame: state => {
114 | state.isGameOver = false;
115 | state.isPaused = false;
116 | state.questions = [];
117 | state.round = 0;
118 | state.scores.playerOne.total = 0;
119 | state.scores.playerTwo.total = 0;
120 | state.scores.playerOne.history = [];
121 | state.scores.playerTwo.history = [];
122 | },
123 | // Score after answering question
124 | score: (state, payload) => {
125 | let player = payload.mode;
126 | if (payload.true) {
127 | state.scores[player].history.push({
128 | correct: true,
129 | incorrect: false,
130 | });
131 | state.scores[player].total += 1;
132 | } else {
133 | state.scores[player].history.push({
134 | correct: false,
135 | incorrect: true,
136 | });
137 | }
138 | },
139 | // Set game mode from Starter.vue
140 | selectMode: (state, payload) => {
141 | payload === true ? state.solo = true : state.solo = false;
142 | },
143 | // Set current category from Starter.vue
144 | setCurrentCategory: (state, payload) => {
145 | state.currentCategory.name = payload.name;
146 | state.currentCategory.id = payload.id;
147 | },
148 | // Call starPower
149 | starPower: (state) => {
150 | state.starPower = true;
151 | },
152 | startGame: (state, payload) => {
153 | // Set questions to payload from http request in startGame action
154 | state.questions = payload;
155 | // Create list of incorrect choices
156 | state.questions.forEach(el => {
157 | el.choices = el.incorrect_answers.reduce((acc, item) => {
158 | acc.push({
159 | text: item,
160 | answer: false,
161 | classes: {
162 | incorrect: false
163 | }
164 | });
165 | return acc;
166 | }, []);
167 | // Add correct answer
168 | el.choices.push({
169 | text: el.correct_answer,
170 | answer: true,
171 | classes: {
172 | correct: false,
173 | }
174 | });
175 | // Shuffle choices
176 | let i = el.choices.length, temp, rand;
177 | while (0 !== i) {
178 | rand = Math.floor(Math.random() * i);
179 | i -= 1;
180 | temp = el.choices[i];
181 | el.choices[i] = el.choices[rand];
182 | el.choices[rand] = temp;
183 | }
184 | });
185 | // Set view to game board
186 | state.currentView = 'app-game-board';
187 | },
188 | // Apply classes, which indicate correct or incorrect, to buttons after
189 | // submtting answer
190 | styleButtons: state => {
191 | state.questions[state.round].choices.forEach(el => {
192 | if (el.answer) {
193 | el.classes = { correct: true }
194 | } else {
195 | el.classes = { incorrect: true }
196 | }
197 | });
198 | }
199 | }
200 | });
201 |
--------------------------------------------------------------------------------
/webpack.config.js:
--------------------------------------------------------------------------------
1 | var path = require('path')
2 | var webpack = require('webpack')
3 |
4 | module.exports = {
5 | entry: './src/main.js',
6 | output: {
7 | path: path.resolve(__dirname, './dist'),
8 | publicPath: '/dist/',
9 | filename: 'build.js'
10 | },
11 | module: {
12 | rules: [
13 | {
14 | test: /\.vue$/,
15 | loader: 'vue-loader',
16 | options: {
17 | loaders: {
18 | // Since sass-loader (weirdly) has SCSS as its default parse mode, we map
19 | // the "scss" and "sass" values for the lang attribute to the right configs here.
20 | // other preprocessors should work out of the box, no loader config like this necessary.
21 | 'scss': 'vue-style-loader!css-loader!sass-loader',
22 | 'sass': 'vue-style-loader!css-loader!sass-loader?indentedSyntax'
23 | }
24 | // other vue-loader options go here
25 | }
26 | },
27 | {
28 | test: /\.js$/,
29 | loader: 'babel-loader',
30 | exclude: /node_modules/
31 | },
32 | {
33 | test: /\.(png|jpg|gif|svg)$/,
34 | loader: 'file-loader',
35 | options: {
36 | name: '[name].[ext]?[hash]'
37 | }
38 | }
39 | ]
40 | },
41 | resolve: {
42 | alias: {
43 | 'vue$': 'vue/dist/vue.esm.js'
44 | }
45 | },
46 | devServer: {
47 | historyApiFallback: true,
48 | noInfo: true
49 | },
50 | performance: {
51 | hints: false
52 | },
53 | devtool: '#eval-source-map'
54 | }
55 |
56 | if (process.env.NODE_ENV === 'production') {
57 | module.exports.devtool = '#source-map'
58 | // http://vue-loader.vuejs.org/en/workflow/production.html
59 | module.exports.plugins = (module.exports.plugins || []).concat([
60 | new webpack.DefinePlugin({
61 | 'process.env': {
62 | NODE_ENV: '"production"'
63 | }
64 | }),
65 | new webpack.optimize.UglifyJsPlugin({
66 | sourceMap: true,
67 | compress: {
68 | warnings: false
69 | }
70 | }),
71 | new webpack.LoaderOptionsPlugin({
72 | minimize: true
73 | })
74 | ])
75 | }
76 |
--------------------------------------------------------------------------------