├── .babelrc
├── .firebaserc
├── .gitignore
├── LICENSE
├── README.md
├── database.rules.json
├── firebase.json
├── index.html
├── package.json
├── screenshot.png
├── sketch
├── dark.png
├── favicon.sketch
└── light.png
├── src
├── App.vue
├── app.js
├── assets
│ ├── close.svg
│ ├── dark
│ │ ├── add.svg
│ │ ├── profile.svg
│ │ ├── search.svg
│ │ ├── share.svg
│ │ └── theme.svg
│ ├── light
│ │ ├── add.svg
│ │ ├── profile.svg
│ │ ├── search.svg
│ │ ├── share.svg
│ │ └── theme.svg
│ └── trash.svg
├── components
│ ├── Dropdown.vue
│ ├── Editor
│ │ ├── EditorHighlight.vue
│ │ └── Index.vue
│ ├── Field.vue
│ ├── Foot
│ │ ├── FootActions.vue
│ │ ├── FootShareNote.vue
│ │ └── Index.vue
│ ├── Message.vue
│ ├── Preview.vue
│ ├── Search
│ │ ├── Index.vue
│ │ ├── SearchInfo.vue
│ │ └── SearchResult.vue
│ └── Spinner.vue
├── directives
│ └── index.js
├── filters
│ └── index.js
├── mixins
│ └── index.js
├── pages
│ ├── Home.vue
│ ├── NotFound.vue
│ ├── app
│ │ ├── NV.vue
│ │ └── Public.vue
│ └── auth
│ │ ├── LogIn.vue
│ │ └── SignUp.vue
├── router
│ └── index.js
├── scss
│ ├── _app.scss
│ ├── _auth.scss
│ ├── _button.scss
│ ├── _dropdown.scss
│ ├── _functions.scss
│ ├── _home.scss
│ ├── _mixins.scss
│ ├── _preview.scss
│ ├── _variables.scss
│ ├── nv
│ │ ├── _editor.scss
│ │ ├── _foot.scss
│ │ └── _search.scss
│ └── styles.scss
└── store
│ ├── api.js
│ ├── constants.js
│ ├── index.js
│ └── modules
│ ├── auth.js
│ └── nv.js
└── webpack.config.js
/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": [
3 | ["es2015", { "modules": false }],
4 | "stage-3"
5 | ]
6 | }
7 |
--------------------------------------------------------------------------------
/.firebaserc:
--------------------------------------------------------------------------------
1 | {
2 | "projects": {
3 | "default": "notational-velocity"
4 | }
5 | }
6 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | node_modules/
3 | dist/
4 | npm-debug.log
5 | settings.js
6 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2017 Thomas Meagher
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.
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Notational, notes at the speed of thought.
2 |
3 | Notational encourages you to keep your hands on the keyboard so you can create, update, and find notes really quickly. The UX is based on the popular macOS app, [Notational Velocity](http://notational.net/) (but not affiliated in any way).
4 |
5 | 
6 |
7 | ## Firebase Setup
8 |
9 | Notational uses Firebase (it's [pretty abstracted so it could change](https://github.com/tmm/notational/blob/master/src/store/api.js)). You can spin up your own instance for testing or local development. [Check out this issue for more info.](https://github.com/tmm/notational/issues/3)
10 |
11 | You will also need to create a `settings.js` file in the root directory that contains your [Firebase](https://firebase.google.com/) information:
12 |
13 | ```js
14 | // settings.js
15 |
16 | const ENV = {
17 | firebase: {
18 | apiKey: "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX",
19 | authDomain: "YOUR-APP-NAME.firebaseapp.com",
20 | databaseURL: "https://YOUR-APP-NAME.firebaseio.com",
21 | storageBucket: "YOUR-APP-NAME.appspot.com",
22 | messagingSenderId: "XXXXXXXXXXXX"
23 | }
24 | }
25 |
26 | export default ENV
27 | global.ENV = ENV
28 | ```
29 |
30 | The initial DB schema should look like the following (`default_notes` is the only required key-value pair):
31 |
32 | ```js
33 | {
34 | "default_notes" : {
35 | "notes" : [
36 | {
37 | "body" : "Hey-oh!\n\nWelcome to Notational – the fastest note-taking app on the web.\n\nNotational makes it easy to keep your hands on the keyboard so you can do things like... type notes really quickly! (See *Useful Shortcuts* above.)\n\nAlso, no need to save your work as everything you type (in this box) is ~automagically~ saved.\n\n—Tom",
38 | "date_created" : "Sun Mar 05 2017 18:15:27 GMT-0500",
39 | "date_modified" : "Sun Mar 05 2017 18:25:02 GMT-0500",
40 | "id" : 1,
41 | "name" : "Welcome (Click Me)"
42 | },
43 | ...
44 | ]
45 | },
46 | "public_notes" : {
47 | // Auto created
48 | },
49 | "users" : {
50 | // Auto created
51 | }
52 | }
53 | ```
54 |
55 | ## Build Setup
56 |
57 | ``` bash
58 | # install dependencies
59 | npm install
60 |
61 | # serve with hot reload at localhost:8080
62 | npm run dev
63 |
64 | # build for production with minification
65 | npm run build
66 |
67 | # deploy on Firebase
68 | npm run deploy
69 | ```
70 |
71 | ## License
72 |
73 | Released under the MIT license. See LICENSE for details.
74 |
--------------------------------------------------------------------------------
/database.rules.json:
--------------------------------------------------------------------------------
1 | {
2 | "rules": {
3 | "default_notes": {
4 | ".read": true,
5 | ".write": false
6 | },
7 | "public_notes": {
8 | ".read": true,
9 | ".write": "auth != null"
10 | },
11 | "users": {
12 | "$uid": {
13 | ".read": "$uid === auth.uid",
14 | ".write": "$uid === auth.uid",
15 | "notes": {
16 | "$id": {
17 | ".read": "data.child('is_public').val() == true"
18 | }
19 | }
20 | }
21 | }
22 | }
23 | }
--------------------------------------------------------------------------------
/firebase.json:
--------------------------------------------------------------------------------
1 | {
2 | "database": {
3 | "rules": "database.rules.json"
4 | },
5 | "hosting": {
6 | "public": "dist",
7 | "rewrites": [
8 | {
9 | "source": "**",
10 | "destination": "/index.html"
11 | }
12 | ]
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Notational
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "nv",
3 | "description": "Notational velocity for the web.",
4 | "version": "1.0.0",
5 | "author": "Tom Meagher ",
6 | "private": true,
7 | "scripts": {
8 | "dev": "cross-env NODE_ENV=development webpack-dev-server --open --hot",
9 | "build": "cross-env NODE_ENV=production webpack --progress --hide-modules",
10 | "deploy": "firebase deploy"
11 | },
12 | "dependencies": {
13 | "clipboard": "^1.6.1",
14 | "firebase": "^3.7.0",
15 | "keyboardjs": "^2.3.3",
16 | "local-storage": "^1.4.2",
17 | "lodash": "^4.17.4",
18 | "moment": "^2.17.1",
19 | "string_score": "^0.1.22",
20 | "vue": "^2.1.0",
21 | "vue-head": "^2.0.10",
22 | "vue-router": "^2.2.1",
23 | "vuex": "^2.1.2"
24 | },
25 | "devDependencies": {
26 | "babel-core": "^6.0.0",
27 | "babel-loader": "^6.0.0",
28 | "babel-preset-es2015": "^6.0.0",
29 | "babel-preset-stage-3": "^6.22.0",
30 | "copy-webpack-plugin": "^4.0.1",
31 | "cross-env": "^3.0.0",
32 | "css-loader": "^0.25.0",
33 | "extract-text-webpack-plugin": "^2.0.0-rc.3",
34 | "file-loader": "^0.9.0",
35 | "node-sass": "^4.5.0",
36 | "sass-loader": "^5.0.1",
37 | "style-loader": "^0.13.1",
38 | "url-loader": "^0.5.8",
39 | "vue-loader": "^10.0.0",
40 | "vue-template-compiler": "^2.1.0",
41 | "webpack": "^2.2.0",
42 | "webpack-dev-server": "^2.4.1"
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/screenshot.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/awkweb/notational/4ec0e8e6da9c224282317b55050b25ce1f26d19a/screenshot.png
--------------------------------------------------------------------------------
/sketch/dark.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/awkweb/notational/4ec0e8e6da9c224282317b55050b25ce1f26d19a/sketch/dark.png
--------------------------------------------------------------------------------
/sketch/favicon.sketch:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/awkweb/notational/4ec0e8e6da9c224282317b55050b25ce1f26d19a/sketch/favicon.sketch
--------------------------------------------------------------------------------
/sketch/light.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/awkweb/notational/4ec0e8e6da9c224282317b55050b25ce1f26d19a/sketch/light.png
--------------------------------------------------------------------------------
/src/App.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
12 |
13 |
--------------------------------------------------------------------------------
/src/app.js:
--------------------------------------------------------------------------------
1 | require('./scss/styles.scss');
2 |
3 | import Vue from 'vue'
4 | import App from './App.vue'
5 | import router from './router'
6 | import store from './store'
7 | import * as directives from './directives'
8 | import * as filters from './filters'
9 |
10 |
11 | Object.keys(filters).forEach(key => { Vue.filter(key, filters[key]) })
12 |
13 | const app = new Vue({
14 | el: '#app',
15 | template: '',
16 | router,
17 | store,
18 | components: { App }
19 | })
--------------------------------------------------------------------------------
/src/assets/close.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/assets/dark/add.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/assets/dark/profile.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/assets/dark/search.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/assets/dark/share.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/assets/dark/theme.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/assets/light/add.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/assets/light/profile.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/assets/light/search.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/assets/light/share.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/assets/light/theme.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/assets/trash.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/components/Dropdown.vue:
--------------------------------------------------------------------------------
1 |
2 |
5 |
9 |
10 |
22 |
23 |
24 |
25 |
88 |
--------------------------------------------------------------------------------
/src/components/Editor/EditorHighlight.vue:
--------------------------------------------------------------------------------
1 |
2 |
10 |
11 |
12 |
56 |
--------------------------------------------------------------------------------
/src/components/Editor/Index.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
18 |
19 |
20 |
23 | No Note Selected
24 |
25 |
26 |
27 |
28 |
29 |
107 |
--------------------------------------------------------------------------------
/src/components/Field.vue:
--------------------------------------------------------------------------------
1 |
2 |
6 |
7 |
8 |
20 |
32 |
33 |
34 |
35 |
64 |
65 |
142 |
--------------------------------------------------------------------------------
/src/components/Foot/FootActions.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | {{ activeNote.body | wordCount }} words,
5 | {{ activeNote.body | charCount }} chars
6 |
7 |
8 |
9 | {{ randomQuote() }}
10 |
11 |
12 |
27 |
28 |
29 |
30 |
91 |
--------------------------------------------------------------------------------
/src/components/Foot/FootShareNote.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
10 |
11 |
17 |
18 |
19 |
20 |
25 |
26 | Anyone with the link can view
27 |
28 |
29 |
30 |
34 |
35 |
36 |
37 |
95 |
--------------------------------------------------------------------------------
/src/components/Foot/Index.vue:
--------------------------------------------------------------------------------
1 |
2 |
13 |
14 |
15 |
52 |
--------------------------------------------------------------------------------
/src/components/Message.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | {{ text }}
5 |
6 |
7 |
9 |
10 |
11 |
12 |
13 |
26 |
27 |
76 |
--------------------------------------------------------------------------------
/src/components/Preview.vue:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
6 |
7 |
8 |
9 |
12 |
13 |
14 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
66 |
--------------------------------------------------------------------------------
/src/components/Search/Index.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
19 |
20 |
28 |
29 |
30 |
36 |
37 |
38 |
51 |
52 |
53 |
54 |
241 |
--------------------------------------------------------------------------------
/src/components/Search/SearchInfo.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | 1 note
5 |
6 |
7 | {{ resultsCount }} notes
8 |
9 |
10 |
11 | esc to focus search
12 |
13 |
14 | enter to save title
15 |
16 |
17 | ctrl + enter to create note
18 |
19 |
20 | enter to edit note
21 |
22 |
23 | Type to search
24 |
25 |
26 |
27 |
28 |
29 |
42 |
--------------------------------------------------------------------------------
/src/components/Search/SearchResult.vue:
--------------------------------------------------------------------------------
1 |
2 |
10 |
11 |
21 |
22 |
23 |
24 |
25 |
28 | – {{ note.body }}
29 |
30 |
31 |
32 |
37 |
38 |
41 | {{ note.date_modified | prettyDate }}
42 |
43 |
44 |
45 |
46 |
47 |
153 |
--------------------------------------------------------------------------------
/src/components/Spinner.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
19 |
20 |
21 |
22 |
23 |
24 |
29 |
30 |
--------------------------------------------------------------------------------
/src/directives/index.js:
--------------------------------------------------------------------------------
1 | import Vue from 'vue'
2 |
3 | Vue.directive('focus', {
4 | inserted: function (el, binding) {
5 | if (binding.value) {
6 | el.focus()
7 | }
8 | }
9 | });
10 |
11 | Vue.directive('click-outside', {
12 | bind: function (el, binding, vnode) {
13 | el.event = function (event) {
14 | if (!(el == event.target || el.contains(event.target))) {
15 | vnode.context[binding.expression](event)
16 | }
17 | }
18 | document.body.addEventListener('click', el.event)
19 | },
20 | unbind: function (el) {
21 | document.body.removeEventListener('click', el.event)
22 | }
23 | });
--------------------------------------------------------------------------------
/src/filters/index.js:
--------------------------------------------------------------------------------
1 | import moment from 'moment'
2 |
3 | export function prettyDate (dateString) {
4 | return moment.parseZone(dateString, 'ddd MMM DD YYYY HH:mm:ss ZZ')
5 | .local()
6 | .fromNow()
7 | }
8 |
9 | export function wordCount (string) {
10 | return string ? (string.replace(/['";:,.?¿\-!¡]+/g, '').match(/\S+/g) || []).length : 0
11 | }
12 |
13 | export function charCount (string) {
14 | return string ? string.replace(/\s/g, '').length : 0
15 | }
16 |
--------------------------------------------------------------------------------
/src/mixins/index.js:
--------------------------------------------------------------------------------
1 | import ls from 'local-storage'
2 | import moment from 'moment'
3 | import _ from 'lodash'
4 | import 'string_score'
5 |
6 | export const localStorageMixin = {
7 | methods: {
8 | ls_pushUser (user) {
9 | ls.set('user', user)
10 | },
11 |
12 | ls_pullUser () {
13 | return ls.get('user')
14 | },
15 |
16 | ls_logOut () {
17 | return ls.clear()
18 | }
19 | }
20 | }
21 |
22 |
23 | export const noteMixin = {
24 | methods: {
25 | createNote (id, name, body = '', dateModified = moment()) {
26 | return {
27 | id: id,
28 | name: name,
29 | body: body,
30 | date_modified: dateModified.toString(),
31 | date_created: moment().toString(),
32 | is_public: false
33 | }
34 | },
35 |
36 | findKeyForNoteId (noteId, notes) {
37 | return _.findKey(notes, { 'id': noteId })
38 | },
39 |
40 | nextIdForNotes (notes) {
41 | const ids = _.map(notes, (note) => { return note.id });
42 | return ids.length > 0 ? Math.max(...ids) + 1 : 1
43 | },
44 |
45 | filterNotesForQuery (query, notes) {
46 | return _.filter(notes, (note) => {
47 | const queryLower = query.toLowerCase()
48 | const nameScore = note.name.toLowerCase().score(queryLower)
49 | const bodyScore = note.body.toLowerCase().score(queryLower)
50 | note.score = nameScore + bodyScore
51 | return nameScore > 0 || bodyScore > 0
52 | })
53 | },
54 |
55 | sortNotes (notes, useScore) {
56 | let now = moment()
57 | if (useScore)
58 | return _.orderBy(notes, ['score', (note) => this.secondsFromNow(now, note.date_modified)], ['desc', 'asc'])
59 | else
60 | return _.orderBy(notes, [(note) => this.secondsFromNow(now, note.date_modified)], ['asc'])
61 | },
62 |
63 | secondsFromNow (now, dateString) {
64 | const date = moment(dateString)
65 | return now.diff(date, 'seconds')
66 | }
67 | }
68 | }
69 |
70 |
71 | export const utilsMixin = {
72 | methods: {
73 | selectElement (selector) {
74 | return document.querySelector(`${selector}`)
75 | },
76 |
77 | focusElement (selector) {
78 | const element = this.selectElement(selector)
79 | element.focus()
80 | }
81 | }
82 | }
83 |
--------------------------------------------------------------------------------
/src/pages/Home.vue:
--------------------------------------------------------------------------------
1 |
2 |
6 |
36 |
37 |
38 |
39 |
40 |
41 | Notes at the speed of thought.
42 |
43 |
44 |
61 |
62 |
66 |
67 |
68 |
69 |
85 |
86 |
87 |
88 | Smart shortcuts, magic save, and incremental search keep your hands on the keyboard—and your brain happy.
89 |
90 |
91 |
94 |
95 |
96 |
97 |
200 |
--------------------------------------------------------------------------------
/src/pages/NotFound.vue:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 | Looking for something?
6 |
7 |
8 |
9 | Unfortunately it's not here. Good luck on your wanderings.
10 |
11 |
12 |
14 | Back to safety
15 |
16 |
17 |
18 |
19 |
30 |
31 |
69 |
--------------------------------------------------------------------------------
/src/pages/app/NV.vue:
--------------------------------------------------------------------------------
1 |
2 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
80 |
--------------------------------------------------------------------------------
/src/pages/app/Public.vue:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
6 |
7 |
8 |
9 |
13 |
14 |
16 |
17 |
18 |
21 |
22 |
23 |
24 |
25 |
75 |
76 |
145 |
--------------------------------------------------------------------------------
/src/pages/auth/LogIn.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
Welcome Back
4 |
5 |
9 |
10 |
11 |
28 |
29 |
31 | New here? Sign up
32 |
33 |
34 |
35 |
36 |
37 |
--------------------------------------------------------------------------------
/src/pages/auth/SignUp.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
Sign Up
4 |
5 |
8 |
9 |
10 |
27 |
28 |
30 | Have an account? Log in
31 |
32 |
33 |
34 |
35 |
36 |
137 |
138 |
--------------------------------------------------------------------------------
/src/router/index.js:
--------------------------------------------------------------------------------
1 | import Vue from 'vue'
2 | import VueHead from 'vue-head'
3 | import VueRouter from 'vue-router'
4 | import ls from 'local-storage'
5 |
6 | import Home from '../pages/Home.vue'
7 | import NotFound from '../pages/NotFound.vue'
8 | import LogIn from '../pages/auth/LogIn.vue'
9 | import SignUp from '../pages/auth/SignUp.vue'
10 | import NV from '../pages/app/NV.vue'
11 | import Public from '../pages/app/Public.vue'
12 |
13 | Vue.use(VueHead)
14 | Vue.use(VueRouter)
15 |
16 | const router = new VueRouter({
17 | mode: 'history',
18 | routes: [
19 | { path: '/', name: 'home', component: Home },
20 | { path: '/app', name: 'app', component: NV, meta: { requiresAuth: true } },
21 | { path: '/login', name: 'login', component: LogIn },
22 | { path: '/signup', name: 'signup', component: SignUp },
23 | { path: '/n/:id', name: 'public', component: Public },
24 | { path: '*', name: 'notfound', component: NotFound }
25 | ]
26 | })
27 |
28 | router.beforeEach((to, from, next) => {
29 | const user = ls.get('user')
30 | if (to.matched.some(record => record.meta.requiresAuth)) {
31 | if (user) {
32 | next()
33 | } else {
34 | next({ name: 'login', query: { redirect: to.fullPath } })
35 | }
36 | } else {
37 | if (user && to.name !== 'public') {
38 | next({ name: 'app' })
39 | } else {
40 | next()
41 | }
42 | }
43 | })
44 |
45 | export default router
46 |
--------------------------------------------------------------------------------
/src/scss/_app.scss:
--------------------------------------------------------------------------------
1 | #nv, #home {
2 | min-height: 100vh;
3 | }
4 |
5 | #login, #signup, #notfound, #public, .container {
6 | margin: {
7 | bottom: 0;
8 | left: auto;
9 | right: auto;
10 | }
11 | padding: {
12 | top: 1rem;
13 | bottom: 1rem;
14 | right: 1rem;
15 | left: 1rem;
16 | }
17 | }
18 |
19 | #login, #signup, #public, .container {
20 | margin: {
21 | top: 0;
22 | }
23 | }
24 |
25 | #notfound, #public, .container {
26 | max-width: 35rem;
27 | }
28 |
29 | #login, #signup {
30 | max-width: 22;
31 | }
32 |
33 | #nv, #home {
34 | color: palette(black);
35 | background-color: palette(white);
36 | transition: background-color $transition;
37 |
38 | &.dark {
39 | color: palette(white);
40 | background: {
41 | color: palette(dark);
42 | }
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/src/scss/_auth.scss:
--------------------------------------------------------------------------------
1 | .auth {
2 | display: flex;
3 | flex-direction: column;
4 | margin: {
5 | bottom: 0;
6 | left: auto;
7 | right: auto;
8 | top: 2rem;
9 | }
10 | max-width: 22rem;
11 |
12 | &__title {
13 | color: palette(black);
14 | font: {
15 | size: 1.9rem;
16 | }
17 | }
18 |
19 | &__form {
20 | flex-direction: column;
21 | margin: {
22 | bottom: 3rem;
23 | }
24 |
25 | &__button {
26 | color: #fff;
27 | cursor: pointer;
28 | background: {
29 | color: palette(orange);
30 | }
31 | border: {
32 | color: palette(orange, dark);
33 | radius: $border-radius;
34 | style: solid;
35 | width: 1px;
36 | }
37 | box-shadow: $box-shadow;
38 | padding: {
39 | bottom: 9px;
40 | right: 11px;
41 | left: 11px;
42 | top: 9px;
43 | }
44 | font: {
45 | family: $sans-serif;
46 | size: 1rem;
47 | weight: 700;
48 | }
49 | margin-top: .5rem;
50 | transition: border-color $transition;
51 | min-width: 8rem;
52 |
53 | &:focus {
54 | outline: 0;
55 | }
56 |
57 | &:hover {
58 | border-color: palette(orange, x-dark);
59 | }
60 | }
61 | }
62 |
63 | a {
64 | outline: 0;
65 | align-self: center;
66 | border-bottom: {
67 | color: palette(gray, light);
68 | style: solid;
69 | width: 1px;
70 | }
71 | color: palette(gray);
72 | font: {
73 | family: $sans-serif;
74 | size: .95rem;
75 | }
76 | text-decoration: none;
77 | }
78 | }
79 |
--------------------------------------------------------------------------------
/src/scss/_button.scss:
--------------------------------------------------------------------------------
1 | .button {
2 | @include border(palette(gray, light));
3 |
4 | background-color: palette(white);
5 | color: palette(gray, dark);
6 | cursor: pointer;
7 | font: {
8 | family: $sans-serif;
9 | size: .85rem;
10 | weight: 500;
11 | }
12 | padding: {
13 | bottom: 3px;
14 | left: 6px;
15 | right: 6px;
16 | top: 3px;
17 | }
18 | transition: background-color $transition, border-color $transition, color $transition;
19 |
20 | &:focus {
21 | outline: 0;
22 | }
23 |
24 | &:hover {
25 | color: palette(black);
26 | }
27 |
28 | &.primary {
29 | background-color: palette(orange);
30 | border-color: palette(orange, dark);
31 | color: palette(white);
32 |
33 | &:hover {
34 | border-color: palette(orange, x-dark);
35 | }
36 | }
37 |
38 | &.large {
39 | box-shadow: $box-shadow;
40 | padding: {
41 | bottom: 9px;
42 | right: 11px;
43 | left: 11px;
44 | top: 9px;
45 | }
46 | font: {
47 | family: $sans-serif;
48 | size: 1rem;
49 | weight: 700;
50 | }
51 | min-width: 8rem;
52 | }
53 | }
54 |
55 | .dark .button {
56 | background-color: palette(dark);
57 | border-color: palette(dark, light);
58 | color: palette(white);
59 |
60 | &.primary {
61 | background-color: palette(blue);
62 | border-color: palette(blue, dark);
63 |
64 | &:hover {
65 | border-color: palette(blue, x-dark);
66 | }
67 | }
68 | }
69 |
70 | .button-icon {
71 | height: 27px;
72 | width: 27px;
73 | background: {
74 | color: transparent;
75 | size: cover;
76 | repeat: no-repeat;
77 | }
78 | border: {
79 | color: palette(gray, light);
80 | radius: $border-radius;
81 | style: solid;
82 | width: 1px;
83 | }
84 | cursor: pointer;
85 | margin: {
86 | left: .25rem;
87 | }
88 | transition: background-image $transition;
89 |
90 | &.user {
91 | background-image: url("../assets/light/profile.svg");
92 | }
93 |
94 | &.share {
95 | background-image: url("../assets/light/share.svg");
96 | }
97 |
98 | &.theme {
99 | background-image: url("../assets/light/theme.svg");
100 | }
101 |
102 | &:focus {
103 | outline: 0;
104 | }
105 | }
106 |
107 | .dark .button-icon {
108 | background-color: transparent;
109 | border-color: palette(dark, light);
110 | transition: background $transition, border-color $transition;
111 |
112 | &.user {
113 | background-image: url("../assets/dark/profile.svg");
114 | }
115 |
116 | &.share {
117 | background-image: url("../assets/dark/share.svg");
118 | }
119 |
120 | &.theme {
121 | background-image: url("../assets/dark/theme.svg");
122 | }
123 | }
124 |
--------------------------------------------------------------------------------
/src/scss/_dropdown.scss:
--------------------------------------------------------------------------------
1 | .dropdown {
2 | position: relative;
3 |
4 | &__menu {
5 | z-index: 10;
6 | color: palette(black);
7 | position: absolute;
8 | top: -65px;
9 | background-color: palette(white);
10 | @include border(palette(gray, light));
11 | margin: 0;
12 | right: 0;
13 | padding: 0;
14 | min-width: 7rem;
15 |
16 | li {
17 | white-space: nowrap;
18 | cursor: pointer;
19 | list-style-type: none;
20 | padding: {
21 | top: .35rem;
22 | bottom: .35rem;
23 | left: .75rem;
24 | right: .75rem;
25 | }
26 | transition: background-color $transition;
27 |
28 | &:hover {
29 | background-color: palette(gray, x-light);
30 | }
31 | }
32 | }
33 | }
34 |
35 | .dark .dropdown {
36 | &__menu {
37 | color: palette(white);
38 | background-color: palette(dark);
39 | border-color: palette(dark, light);
40 |
41 | li {
42 | &:hover {
43 | background-color: palette(dark, d-light);
44 | }
45 |
46 | &:first-child {
47 | border-top-right-radius: $border-radius;
48 | border-top-left-radius: $border-radius;
49 | }
50 |
51 | &:last-child {
52 | border-bottom-right-radius: $border-radius;
53 | border-bottom-left-radius: $border-radius;
54 | }
55 | }
56 | }
57 | }
--------------------------------------------------------------------------------
/src/scss/_functions.scss:
--------------------------------------------------------------------------------
1 | @function palette($palette, $tone: 'base') {
2 | @return map-get(map-get($palettes, $palette), $tone);
3 | }
--------------------------------------------------------------------------------
/src/scss/_home.scss:
--------------------------------------------------------------------------------
1 | .home {
2 | color: palette(gray, dark);
3 |
4 | &__container, &__subline, &__foot {
5 | margin: {
6 | right: auto;
7 | left: auto;
8 | }
9 | padding: {
10 | right: 1rem;
11 | left: 1rem;
12 | }
13 | }
14 |
15 | &__container {
16 | display: flex;
17 | justify-content: space-between;
18 | align-items: center;
19 | max-width: 62rem;
20 |
21 | &.nav {
22 | width: 62rem;
23 | padding: {
24 | top: .5rem;
25 | bottom: .5rem;
26 | }
27 | }
28 |
29 | &.main {
30 | margin-bottom: 1rem;
31 |
32 | @media screen and (max-width: $md) {
33 | flex-direction: column;
34 | }
35 | }
36 | }
37 |
38 | &__nav {
39 | display: flex;
40 | justify-content: space-between;
41 | align-items: center;
42 | margin-bottom: 2rem;
43 | padding: {
44 | top: .5rem;
45 | bottom: 0rem;
46 | }
47 | border-bottom: {
48 | style: solid;
49 | width: 1px;
50 | color: palette(gray, light);
51 | }
52 | transition: border-color $transition;
53 |
54 | &__logo {
55 | color: palette(orange);
56 | text-decoration: none;
57 | font: {
58 | family: $sans-serif;
59 | size: 1.5rem;
60 | weight: 700;
61 | }
62 | transition: color $transition;
63 | outline: 0;
64 |
65 | @media screen and (max-width: $sm) {
66 | font-weight: 900;
67 | span {
68 | display: none;
69 | }
70 | }
71 | }
72 |
73 | &__button {
74 | color: palette(gray, dark);
75 | cursor: pointer;
76 | border: 0;
77 | background-color: transparent;
78 | font: {
79 | family: $sans-serif;
80 | size: 1rem;
81 | weight: 700;
82 | }
83 | padding: {
84 | bottom: 6px;
85 | right: 10px;
86 | left: 10px;
87 | top: 6px;
88 | }
89 | margin-left: 5px;
90 | outline: 0;
91 | text-decoration: none;
92 | transition: color $transition;
93 |
94 | &.primary {
95 | margin-left: 15px;
96 | box-shadow: $box-shadow;
97 | color: palette(white);
98 | border: {
99 | style: solid;
100 | width: 1px;
101 | color: palette(orange, dark);
102 | radius: $border-radius;
103 | }
104 | background-color: palette(orange);
105 | transition: background-color $transition, border-color $transition;
106 |
107 | &:hover {
108 | border-color: palette(orange, x-dark);
109 | }
110 | }
111 | }
112 | }
113 |
114 | &__content {
115 | flex: 1;
116 |
117 | &.left {
118 | max-width: 25rem;
119 | padding-right: 1rem;
120 |
121 | @media screen and (max-width: $md) {
122 | min-width: 100%;
123 | padding-right: 0;
124 | margin-bottom: 1rem;
125 | }
126 | }
127 |
128 | &.right {
129 | max-width: 30rem;
130 | min-width: 30rem;
131 | @media screen and (max-width: $md) {
132 | max-width: 100%;
133 | min-width: 100%;
134 | }
135 | }
136 | }
137 |
138 | .message {
139 | margin-bottom: 0;
140 | }
141 |
142 | &__headline {
143 | color: palette(black);
144 | margin: {
145 | top: 0;
146 | bottom: 2rem;
147 | }
148 | font-weight: 900;
149 | transition: color $transition;
150 | }
151 |
152 | &__browser {
153 | box-shadow: $box-shadow-2;
154 |
155 | margin: {
156 | top: 1rem;
157 | bottom: 2rem;
158 | }
159 |
160 | border: {
161 | style: solid;
162 | width: 1px;
163 | color: palette(orange);
164 | radius: $border-radius;
165 | }
166 | transition: border-color $transition;
167 |
168 | &__header {
169 | display: flex;
170 | align-items: center;
171 | justify-content: space-between;
172 | height: 2rem;
173 | border-bottom: {
174 | style: solid;
175 | width: 1px;
176 | color: palette(orange);
177 | }
178 | transition: border-color $transition;
179 | }
180 |
181 | &__button-group {
182 | padding: {
183 | left: 10px;
184 | }
185 | }
186 |
187 | &__button {
188 | display: inline-block;
189 | width: .5rem;
190 | height: .5rem;
191 | border: {
192 | style: solid;
193 | width: 1px;
194 | color: palette(orange);
195 | radius: 50%;
196 | }
197 | transition: border-color $transition;
198 | }
199 |
200 | &__name {
201 | color: palette(orange);
202 | padding-right: 10px;
203 | font: {
204 | family: $mono;
205 | size: .9rem;
206 | }
207 | transition: color $transition;
208 | }
209 |
210 | &__body {
211 | img {
212 | top: 0;
213 | left: 0;
214 | width: 100%;
215 | max-width: 100%;
216 | height: auto;
217 | border: 0;
218 | }
219 | }
220 | }
221 |
222 | &__subline {
223 | max-width: 45rem;
224 | line-height: 1.5;
225 | text-align: center;
226 | color: palette(black);
227 | font: {
228 | family: $sans-serif;
229 | size: 1.7rem;
230 | }
231 | margin-bottom: 2rem;
232 | transition: color $transition;
233 |
234 | span {
235 | white-space: nowrap;
236 | background-color: palette(orange, light);
237 | font-weight: 700;
238 | transition: background-color $transition;
239 | }
240 |
241 | @media screen and (max-width: $sm) {
242 | font-size: 1.5rem;
243 | }
244 | }
245 |
246 | &__foot {
247 | padding: {
248 | top: 1rem;
249 | bottom: 1rem;
250 | }
251 | text-align: center;
252 | color: palette(gray, light);
253 | font: {
254 | family: $sans-serif;
255 | size: .85rem;
256 | }
257 | transition: color $transition;
258 |
259 | &:hover {
260 | color: palette(gray);
261 | a {
262 | color: palette(gray);
263 | }
264 | }
265 |
266 | a {
267 | color: palette(gray, light);
268 | transition: color $transition;
269 |
270 | &:hover {
271 | color: palette(orange);
272 | }
273 | }
274 | }
275 | }
276 |
277 | .dark .home {
278 | &__nav {
279 | border-bottom-color: palette(dark, light);
280 |
281 | &__logo {
282 | color: palette(blue);
283 | }
284 |
285 | &__button {
286 | color: palette(white);
287 |
288 | &.primary {
289 | background-color: palette(blue);
290 | border-color: palette(blue, dark);
291 |
292 | &:hover {
293 | border-color: palette(blue, x-dark);
294 | }
295 | }
296 | }
297 | }
298 |
299 | &__headline {
300 | color: palette(white);
301 | }
302 |
303 | &__browser {
304 | border-color: palette(blue);
305 |
306 | &__header {
307 | border-color: palette(blue);
308 | }
309 |
310 | &__button {
311 | border-color: palette(blue);
312 | color: palette(blue, dark);
313 | }
314 |
315 | &__name {
316 | color: palette(blue);
317 | }
318 | }
319 |
320 | &__subline {
321 | color: palette(white);
322 |
323 | span {
324 | background-color: palette(blue, a-light);
325 | }
326 | }
327 |
328 | &__foot {
329 | color: palette(dark, light);
330 |
331 | a {
332 | color: palette(dark, light);
333 |
334 | &:hover {
335 | color: palette(blue);
336 | }
337 | }
338 | }
339 | }
340 |
--------------------------------------------------------------------------------
/src/scss/_mixins.scss:
--------------------------------------------------------------------------------
1 | @mixin border($color) {
2 | border: {
3 | color: $color;
4 | radius: $border-radius;
5 | width: 1px;
6 | style: solid;
7 | }
8 | }
9 |
10 | @mixin background($color, $image, $size, $position) {
11 | background: {
12 | color: $color;
13 | image: url($image);
14 | size: $size;
15 | position: $position;
16 | repeat: no-repeat;
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/src/scss/_preview.scss:
--------------------------------------------------------------------------------
1 | .preview {
2 | height: 25.65rem;
3 | border-radius: $border-radius;
4 |
5 | .container {
6 | padding-bottom: .75rem;
7 | }
8 |
9 | .search {
10 | &__container {
11 | margin-top: 0;
12 | height: 4rem;
13 | }
14 |
15 | &__input {
16 | background-size: 24px;
17 | font-size: .95rem;
18 | padding: {
19 | bottom: 8px;
20 | right: 30px;
21 | left: 9px;
22 | top: 8px;
23 | }
24 | }
25 |
26 | &__info {
27 | font-size: .7rem;
28 | margin: {
29 | left: .25rem;
30 | right: .25rem;
31 | top: .25rem;
32 | }
33 | }
34 |
35 | &__results {
36 | height: 6rem;
37 | margin-bottom: .15rem;
38 | }
39 |
40 | &__result {
41 | font-size: .9rem;
42 |
43 | &__editor {
44 | font-size: .9rem;
45 | }
46 | }
47 | }
48 |
49 | .editor {
50 | margin-bottom: .35rem;
51 |
52 | &__textarea, &__ghost, &__placeholder {
53 | font-size: .9rem;
54 | height: 10.25rem;
55 | padding: {
56 | bottom: .75rem;
57 | top: .75rem;
58 | }
59 | }
60 |
61 | &__ghost {
62 | width: calc(100% - 2rem);
63 | max-width: 35rem;
64 | }
65 | }
66 |
67 | .foot {
68 | font-size: .8rem;
69 | }
70 |
71 | .button-icon {
72 | height: 26px;
73 | width: 26px;
74 | }
75 |
76 | .dropdown {
77 | &__menu {
78 | top: -63px;
79 | }
80 | }
81 | }
82 |
83 |
84 |
85 |
86 |
87 |
88 |
89 |
--------------------------------------------------------------------------------
/src/scss/_variables.scss:
--------------------------------------------------------------------------------
1 | // Colors
2 | $orange: #FF5722;
3 | $blue: #0087F8;
4 | $dark: #222222;
5 |
6 | $palettes: (
7 | white: (
8 | base: #ffffff
9 | ),
10 |
11 | black: (
12 | base: #000000
13 | ),
14 |
15 | orange: (
16 | base: $orange,
17 | dark: darken($orange, 10),
18 | light: lighten($orange, 35),
19 | a-light: rgba(255,87,34,.25),
20 | x-dark: darken($orange, 20)
21 | ),
22 |
23 | blue: (
24 | base: $blue,
25 | dark: darken($blue, 10),
26 | a-light: rgba(22,121,255,.5),
27 | x-dark: darken($blue, 20)
28 | ),
29 |
30 | green: (
31 | base: #00b27f
32 | ),
33 |
34 | red: (
35 | base: #FA5744
36 | ),
37 |
38 | gray: (
39 | base: #888888,
40 | dark: #555555,
41 | light: #DDDDDD,
42 | x-light: #EFEFEF
43 | ),
44 |
45 | dark: (
46 | base: $dark,
47 | d-light: lighten($dark, 8),
48 | light: lighten($dark, 30),
49 | x-light: lighten($dark, 70)
50 | )
51 | );
52 |
53 | // Type
54 | $sans-serif: 'Avenir', 'Lato', sans-serif;
55 | $mono: 'Inconsolata', 'Lucida Console', monospace;
56 |
57 | // Misc
58 | $border-radius: 3px;
59 | $box-shadow: 0 2px 4px rgba(0,0,0,0.04);
60 | $box-shadow-2: 0 4px 6px rgba(0,0,0,0.07);
61 | $transition: .25s ease-in-out;
62 | $sm: 544px;
63 | $md: 775px;
64 |
--------------------------------------------------------------------------------
/src/scss/nv/_editor.scss:
--------------------------------------------------------------------------------
1 | .editor {
2 | display: flex;
3 | margin-bottom: .75rem;
4 |
5 | mark {
6 | background-color: palette(orange, a-light);
7 | color: transparent;
8 | transition: background-color $transition;
9 | }
10 | .dark & mark {
11 | background-color: palette(blue, a-light);
12 | }
13 |
14 | &__textarea {
15 | z-index: 1;
16 | -webkit-appearance: none;
17 | }
18 |
19 | &__textarea, &__ghost, &__placeholder {
20 | border: {
21 | top: 1px solid;
22 | bottom: 1px solid;
23 | right: 0;
24 | left: 0;
25 | }
26 | flex: 1;
27 | font: {
28 | size: .95rem;
29 | family: $sans-serif;
30 | }
31 | height: 15.5rem;
32 | padding: {
33 | bottom: 1rem;
34 | left: 0;
35 | right: 0;
36 | top: 1rem;
37 | }
38 | }
39 |
40 | &__textarea, &__placeholder {
41 | border-color: palette(gray, light);
42 | }
43 | .dark &__textarea, .dark &__placeholder {
44 | border-color: palette(dark, light);
45 | }
46 |
47 | &__textarea {
48 | background-color: transparent;
49 | border-radius: 0;
50 | color: palette(black);
51 | margin: 0;
52 | outline: 0;
53 | resize: none;
54 | transition: border-color $transition, color $transition;
55 |
56 | &:focus {
57 | border-color: palette(orange);
58 | }
59 |
60 | &::placeholder {
61 | color: palette(gray);
62 | }
63 | }
64 | .dark &__textarea, &__placeholder {
65 | color: palette(white);
66 |
67 | &:focus {
68 | border-color: palette(blue);
69 | }
70 |
71 | &::placeholder {
72 | color: palette(dark, x-light);
73 | }
74 | }
75 |
76 | &__ghost {
77 | width: calc(100% - 2rem);
78 | max-width: 35rem;
79 | background-color: transparent;
80 | border-color: transparent;
81 | color: transparent;
82 | position: absolute;
83 | pointer-events: none;
84 |
85 | .highlights {
86 | height: 100%;
87 | overflow: hidden;
88 | white-space: pre-wrap;
89 | word-wrap: break-word;
90 | }
91 | }
92 |
93 | &__placeholder {
94 | align-items: center;
95 | color: palette(gray, dark);
96 | display: flex;
97 | justify-content: center;
98 | transition: border-color $transition, color $transition;
99 | }
100 | .dark &__placeholder {
101 | color: palette(dark, x-light);
102 | }
103 | }
104 |
--------------------------------------------------------------------------------
/src/scss/nv/_foot.scss:
--------------------------------------------------------------------------------
1 | .foot {
2 | align-items: center;
3 | display: flex;
4 | color: palette(gray, dark);
5 | font: {
6 | weight: 500;
7 | size: .85rem;
8 | }
9 | justify-content: space-between;
10 |
11 | &__right {
12 | display: flex;
13 | align-items: flex-start;
14 | }
15 | transition: color $transition;
16 | }
17 |
18 | .dark .foot {
19 | color: palette(dark, x-light);
20 | }
21 |
22 | .main-actions, .share-note, .user-profile {
23 | flex: 1;
24 | display: flex;
25 | justify-content: space-between;
26 |
27 | &__right {
28 | display: flex;
29 | }
30 | }
31 |
32 | .main-actions {
33 | align-items: center;
34 | }
35 |
36 | .share-note {
37 | align-items: flex-start;
38 |
39 | &__container {
40 | display: flex;
41 | flex-direction: column;
42 | }
43 |
44 | &__copy {
45 | display: flex;
46 | margin: {
47 | bottom: .35rem;
48 | }
49 | }
50 |
51 | &__url {
52 | cursor: default;
53 | width: 275px;
54 | text-overflow: ellipsis;
55 | color: palette(black);
56 | font: {
57 | size: .8rem;
58 | family: $sans-serif;
59 | }
60 | background-color: palette(white);
61 | border: {
62 | width: 1px;
63 | style: solid;
64 | color: palette(gray, light);
65 | right-width: 0;
66 | top-left-radius: $border-radius;
67 | bottom-left-radius: $border-radius;
68 | top-right-radius: 0;
69 | bottom-right-radius: 0;
70 | }
71 | padding: {
72 | top: 3px;
73 | left: 6px;
74 | right: 6px;
75 | bottom: 3px;
76 | }
77 | -webkit-appearance: none;
78 |
79 | &:focus {
80 | outline: 0;
81 | }
82 |
83 | @media screen and (max-width: $sm) {
84 | width: 185px;
85 | }
86 | }
87 |
88 | .dark &__url {
89 | color: palette(white);
90 | border-color: palette(dark, light);
91 | background-color: palette(dark);
92 | }
93 |
94 | &__link {
95 | color: palette(orange);
96 | }
97 |
98 | .dark &__link {
99 | color: palette(blue);
100 | }
101 |
102 | &__button {
103 | cursor: pointer;
104 | background-color: palette(orange);
105 | color: palette(white);
106 | cursor: pointer;
107 | border: {
108 | width: 1px;
109 | style: solid;
110 | color: palette(orange, dark);
111 | top-right-radius: $border-radius;
112 | bottom-right-radius: $border-radius;
113 | }
114 | font: {
115 | size: .85rem;
116 | weight: 500;
117 | family: $sans-serif;
118 | }
119 |
120 | padding: {
121 | top: 3px;
122 | left: 6px;
123 | right: 6px;
124 | bottom: 3px;
125 | }
126 |
127 | &:focus {
128 | outline: 0;
129 | }
130 | }
131 |
132 | .dark &__button {
133 | background-color: palette(blue);
134 | border: {
135 | color: palette(blue, dark);
136 | }
137 | }
138 |
139 | &__permission {
140 | display: flex;
141 | align-items: center;
142 | }
143 |
144 | &__checkbox {
145 | cursor: pointer;
146 | background-color: transparent;
147 | border: {
148 | style: solid;
149 | width: 1px;
150 | radius: $border-radius;
151 | }
152 | margin: {
153 | right: .25rem;
154 | }
155 | font: {
156 | weight: 500;
157 | family: $sans-serif;
158 | }
159 | height: 23px;
160 | width: 27px;
161 | padding: 0;
162 | transition: border-color $transition, color $transition;
163 |
164 | &:focus {
165 | outline: 0;
166 | }
167 |
168 | &.on {
169 | color: palette(green);
170 | border: {
171 | color: palette(green);
172 | }
173 | }
174 |
175 | &.off {
176 | color: palette(red);
177 | border: {
178 | color: palette(red);
179 | }
180 | }
181 | }
182 |
183 | &__info {
184 | font: {
185 | size: .8rem;
186 | weight: 500;
187 | }
188 | }
189 | }
190 |
--------------------------------------------------------------------------------
/src/scss/nv/_search.scss:
--------------------------------------------------------------------------------
1 | .search {
2 | display: flex;
3 | flex-direction: column;
4 | position: relative;
5 |
6 | mark {
7 | background-color: palette(orange, a-light);
8 | color: palette(black);
9 | transition: background-color $transition, color $transition;
10 | }
11 |
12 | .dark & mark {
13 | background-color: palette(blue, a-light);
14 | color: palette(white);
15 | }
16 |
17 | &__container {
18 | display: flex;
19 | flex-direction: column;
20 | height: 4.5rem;
21 | margin: {
22 | bottom: 0;
23 | left: 0;
24 | right: 0;
25 | top: 1rem;
26 | }
27 |
28 | @media screen and (max-width: $sm) {
29 | margin: {
30 | top: .25rem;
31 | }
32 | }
33 | }
34 |
35 | &__input {
36 | @include border(palette(gray, light));
37 | @include background(
38 | transparent,
39 | "../assets/light/search.svg",
40 | 25px,
41 | 99%
42 | );
43 |
44 | color: palette(black);
45 | box-shadow: $box-shadow;
46 | flex: 1;
47 | font: {
48 | family: $sans-serif;
49 | size: 1rem;
50 | }
51 | max-height: 25px;
52 | outline: 0;
53 | padding: {
54 | bottom: 9px;
55 | right: 30px;
56 | left: 10px;
57 | top: 9px;
58 | }
59 | transition: border-color $transition, background $transition, color $transition;
60 |
61 | &:focus {
62 | border-color: palette(orange);
63 | }
64 |
65 | &::placeholder {
66 | color: palette(gray);
67 | transition: color $transition;
68 | }
69 | }
70 |
71 | .dark &__input {
72 | border-color: palette(dark, light);
73 | background-image: url("../assets/dark/search.svg");
74 | color: palette(white);
75 |
76 | &:focus {
77 | border-color: palette(blue);
78 | }
79 |
80 | &::placeholder {
81 | color: palette(dark, x-light);
82 | }
83 | }
84 |
85 | &__info {
86 | color: palette(gray, dark);
87 | display: flex;
88 | font-size: .75rem;
89 | justify-content: space-between;
90 | margin: {
91 | left: .4rem;
92 | right: .4rem;
93 | top: .4rem;
94 | }
95 | transition: color $transition;
96 |
97 | .command {
98 | text-transform: uppercase;
99 | color: palette(black);
100 | background-color: palette(gray, x-light);
101 | font-family: $mono;
102 | padding: {
103 | bottom: .05rem;
104 | right: .1rem;
105 | left: .1rem;
106 | top: .05rem;
107 | }
108 | transition: background-color $transition, color $transition;
109 | }
110 | }
111 |
112 | .dark &__info {
113 | color: palette(dark, x-light);
114 |
115 | .command {
116 | color: palette(white);
117 | background-color: palette(dark, d-light);
118 | }
119 | }
120 |
121 | &__add {
122 | background: {
123 | color: transparent;
124 | image: url("../assets/light/add.svg");
125 | repeat: no-repeat;
126 | size: cover;
127 | }
128 | border: 0;
129 | cursor: pointer;
130 | font: {
131 | size: 2rem;
132 | }
133 | height: 35px;
134 | opacity: .35;
135 | outline: 0;
136 | position: absolute;
137 | right: -2.5rem;
138 | top: 1.3rem;
139 | transition: background-image $transition, opacity $transition;
140 | width: 35px;
141 |
142 | &:hover {
143 | opacity: 1;
144 | }
145 |
146 | @media screen and (max-width: 645px) {
147 | right: 2rem;
148 | }
149 |
150 | @media screen and (max-width: $sm) {
151 | height: 33px;
152 | right: 1.85rem;
153 | top: .65rem;
154 | width: 33px;
155 | }
156 | }
157 |
158 | .dark &__add {
159 | background-image: url("../assets/dark/add.svg");
160 | }
161 |
162 | &__results {
163 | height: 8rem;
164 | list-style-type: none;
165 | margin: {
166 | bottom: .5rem;
167 | top: 0;
168 | }
169 | overflow-y: auto;
170 | padding-left: 0;
171 | }
172 |
173 | &__result {
174 | color: palette(black);
175 | cursor: pointer;
176 | display: flex;
177 | align-items: center;
178 | font: {
179 | family: $sans-serif;
180 | size: .95rem;
181 | }
182 | justify-content: space-between;
183 | height: 31px;
184 | padding: {
185 | bottom: 0;
186 | right: .4rem;
187 | left: .4rem;
188 | top: 0;
189 | }
190 | transition: color $transition;
191 |
192 | span {
193 | white-space: nowrap;
194 | }
195 |
196 | &.active {
197 | background-color: palette(gray, x-light);
198 | transition: color $transition;
199 | }
200 |
201 | &__name {
202 | overflow: hidden;
203 | text-overflow: ellipsis;
204 | }
205 |
206 | &__description {
207 | color: palette(gray, dark);
208 | transition: color $transition;
209 | }
210 |
211 | &__time {
212 | padding-left: .5rem;
213 | }
214 |
215 | &__delete {
216 | height: 27px;
217 | min-width: 27px;
218 | padding: 0;
219 | outline: 0;
220 | background: {
221 | color: transparent;
222 | repeat: no-repeat;
223 | position: center;
224 | image: url("../assets/trash.svg");
225 | }
226 | border: 0;
227 | cursor: pointer;
228 | }
229 |
230 | &__editor {
231 | background-color: palette(gray, x-light);
232 | border: 0;
233 | color: palette(black);
234 | flex: 1;
235 | font: {
236 | family: $sans-serif;
237 | size: .95rem;
238 | }
239 | padding: 0;
240 | outline: 0;
241 | }
242 | }
243 |
244 | .dark &__result {
245 | color: palette(white);
246 |
247 | &.active {
248 | background-color: palette(dark, d-light);
249 | }
250 |
251 | &__description {
252 | color: palette(dark, x-light);
253 | }
254 |
255 | &__editor {
256 | background-color: palette(dark, d-light);
257 | color: palette(white);
258 | }
259 | }
260 | }
--------------------------------------------------------------------------------
/src/scss/styles.scss:
--------------------------------------------------------------------------------
1 | @import "variables";
2 | @import "mixins";
3 | @import "functions";
4 |
5 | @import "app";
6 | @import "auth";
7 | @import "button";
8 | @import "dropdown";
9 | @import "home";
10 | @import "preview";
11 |
12 | @import "nv/editor";
13 | @import "nv/foot";
14 | @import "nv/search";
15 |
--------------------------------------------------------------------------------
/src/store/api.js:
--------------------------------------------------------------------------------
1 | import firebase from 'firebase'
2 | import _ from 'lodash'
3 | import moment from 'moment'
4 |
5 | import '../../settings'
6 |
7 | const app = firebase.initializeApp(ENV.firebase)
8 | const auth = app.auth()
9 | const database = app.database()
10 |
11 | export default {
12 | logIn (email, password) {
13 | return auth.signInWithEmailAndPassword(email, password)
14 | },
15 |
16 | logOut () {
17 | return auth.signOut()
18 | },
19 |
20 | signUp (email, password) {
21 | return auth.createUserWithEmailAndPassword(email, password)
22 | },
23 |
24 | initNotesForUserId (userId) {
25 | const vm = this
26 | return new Promise((resolve, reject) => {
27 | return this.getDefaultNotes()
28 | .then(notes => {
29 | const dateModified = moment().toString()
30 | _.forEach(notes, function(note, key) {
31 | note.date_modified = dateModified
32 | note.date_created = dateModified
33 | vm.createNote(userId, note)
34 | })
35 | vm.updateTheme(userId, 'light')
36 | resolve(true)
37 | })
38 | })
39 | },
40 |
41 | getDefaultNotes () {
42 | return new Promise((resolve, reject) => {
43 | const notesRef = database.ref('default_notes/notes')
44 | return notesRef.once('value')
45 | .then(res => resolve(res.val()))
46 | })
47 | },
48 |
49 | getDataForUserId (userId) {
50 | return new Promise((resolve, reject) => {
51 | const userRef = database.ref(`users/${userId}`)
52 | return userRef.once('value')
53 | .then(res => resolve(res.val()))
54 | })
55 | },
56 |
57 | getPreviewData () {
58 | return new Promise((resolve, reject) => {
59 | const notesRef = database.ref('default_notes/notes')
60 | return notesRef.once('value')
61 | .then((res) => {
62 | const notes = {}
63 | const dateModified = moment().subtract(1, 'minute').toString()
64 | res.val().forEach((note) => {
65 | note.date_modified = dateModified
66 | notes[note.id] = note
67 | })
68 | resolve({ notes: notes, theme: 'light' })
69 | })
70 | })
71 | },
72 |
73 | createNote (userId, note) {
74 | return new Promise((resolve, reject) => {
75 | const userNotesRef = database.ref(`users/${userId}/notes`)
76 | return userNotesRef.push(note)
77 | .then(res => resolve(res.key))
78 | })
79 | },
80 |
81 | updateNote (userId, key, note) {
82 | return new Promise((resolve, reject) => {
83 | const notesRef = database.ref(`users/${userId}/notes/${key}`)
84 | const dateModified = moment().toString()
85 | return notesRef.update({
86 | name: note.name,
87 | body: note.body,
88 | date_modified: dateModified
89 | })
90 | .then(res => resolve({ key: key, date_modified: dateModified }))
91 | })
92 | },
93 |
94 | deleteNote (userId, key) {
95 | const notesRef = database.ref(`users/${userId}/notes/${key}`)
96 | return notesRef.remove()
97 | },
98 |
99 | updateTheme (userId, theme) {
100 | return new Promise((resolve, reject) => {
101 | const userRef = database.ref(`users/${userId}`)
102 | return userRef.update({theme: theme})
103 | .then(res => resolve(theme))
104 | })
105 | },
106 |
107 | getPublicNoteForId (noteId) {
108 | return new Promise((resolve, reject) => {
109 | const noteRef = database.ref(`public_notes/${noteId}`)
110 | return noteRef.on('value', (snapshot) => {
111 | const data = snapshot.val()
112 | if (data) {
113 | const notesRef = database.ref(`users/${data.user_id}/notes/${data.note_id}`)
114 | notesRef.on('value', (snapshot) => {
115 | const note = snapshot.val()
116 | resolve(note)
117 | })
118 | } else {
119 | reject('Nothing here.')
120 | }
121 | })
122 | })
123 | },
124 |
125 | toggleIsPublic (userId, key, note) {
126 | const is_public = !note.is_public
127 | return new Promise((resolve, reject) => {
128 | const notesRef = database.ref(`users/${userId}/notes/${key}`)
129 | return notesRef.update({
130 | is_public: is_public,
131 | })
132 | .then(res => {
133 | const publicNoteRef = database.ref(`public_notes/${key}`)
134 | if (is_public) {
135 | publicNoteRef.set({
136 | user_id: userId,
137 | note_id: key
138 | })
139 | } else {
140 | publicNoteRef.remove()
141 | }
142 | resolve({ key: key, is_public: is_public })
143 | })
144 | })
145 | }
146 | }
147 |
--------------------------------------------------------------------------------
/src/store/constants.js:
--------------------------------------------------------------------------------
1 | // Root state constants
2 | export const SET_THEME = 'SET_THEME'
3 | export const SET_QUERY = 'SET_QUERY'
4 | export const SET_RESULT_INDEX = 'SET_RESULT_INDEX'
5 | export const SET_RENAMING_ID = 'SET_RENAMING_ID'
6 | export const SET_EDITING_ID = 'SET_EDITING_ID'
7 |
8 | // Auth module constants
9 | export const SET_USER = 'SET_USER'
10 |
11 | // Notes module constants
12 | export const SET_NOTES = 'SET_NOTES'
13 | export const SET_ACTIVE_NOTE = 'SET_ACTIVE_NOTE'
14 | export const SET_ACTIVE_KEY = 'SET_ACTIVE_KEY'
15 | export const CREATE_NOTE = 'CREATE_NOTE'
16 | export const UPDATE_NOTE = 'UPDATE_NOTE'
17 | export const DELETE_NOTE = 'DELETE_NOTE'
18 | export const TOGGLE_IS_PUBLIC = 'TOGGLE_IS_PUBLIC'
--------------------------------------------------------------------------------
/src/store/index.js:
--------------------------------------------------------------------------------
1 | import Vue from 'vue'
2 | import Vuex from 'vuex'
3 |
4 | import auth from './modules/auth'
5 | import nv from './modules/nv'
6 | import { getNotesForUserId } from './api'
7 | import {
8 | SET_ACTIVE_NOTE,
9 | SET_ACTIVE_KEY,
10 | SET_QUERY,
11 | SET_THEME,
12 | SET_NOTES,
13 | SET_RESULT_INDEX,
14 | SET_RENAMING_ID,
15 | SET_EDITING_ID } from './constants'
16 |
17 | Vue.use(Vuex)
18 |
19 | const store = new Vuex.Store({
20 | state: {
21 | query: '',
22 | resultIndex: -1,
23 | renamingId: null,
24 | editingId: null
25 | },
26 |
27 | modules: {
28 | auth,
29 | nv
30 | },
31 |
32 | actions: {
33 | RESET_ACTIVE_NOTE: ({ state, commit, rootState }) => {
34 | commit(SET_RESULT_INDEX, -1)
35 | commit(SET_ACTIVE_NOTE, null)
36 | commit(SET_ACTIVE_KEY, null)
37 | },
38 |
39 | RESET_APP: ({ state, commit, rootState }) => {
40 | commit(SET_THEME, 'light')
41 | commit(SET_QUERY, '')
42 | commit(SET_RESULT_INDEX, -1)
43 | commit(SET_NOTES, [])
44 | commit(SET_ACTIVE_NOTE, null)
45 | commit(SET_ACTIVE_KEY, null)
46 | },
47 | },
48 |
49 | mutations: {
50 | [SET_QUERY] (state, query) {
51 | state.query = query
52 | },
53 |
54 | [SET_RESULT_INDEX] (state, resultIndex) {
55 | state.resultIndex = resultIndex
56 | },
57 |
58 | [SET_RENAMING_ID] (state, renamingId) {
59 | state.renamingId = renamingId
60 | },
61 |
62 | [SET_EDITING_ID] (state, editingId) {
63 | state.editingId = editingId
64 | }
65 | },
66 |
67 | getters: {
68 | query: state => {
69 | return state.query
70 | },
71 |
72 | resultIndex: state => {
73 | return state.resultIndex
74 | },
75 |
76 | renamingId: state => {
77 | return state.renamingId
78 | },
79 |
80 | editingId: state => {
81 | return state.editingId
82 | }
83 | }
84 | })
85 |
86 | export default store
87 |
--------------------------------------------------------------------------------
/src/store/modules/auth.js:
--------------------------------------------------------------------------------
1 | import api from '../api'
2 | import { SET_USER } from '../constants'
3 |
4 | const state = {
5 | user: null
6 | }
7 |
8 | const actions = {
9 | LOG_IN_USER: ({ commit }, data) => {
10 | return api.logIn(data.email, data.password)
11 | .then(user => commit(SET_USER, user))
12 | },
13 |
14 | LOG_OUT_USER: ({ state, commit }) => {
15 | return api.logOut()
16 | .then(() => commit(SET_USER, null))
17 | },
18 |
19 | SIGN_UP_USER: ({ commit }, data) => {
20 | return api.signUp(data.email, data.password)
21 | .then(user => commit(SET_USER, user))
22 | }
23 | }
24 |
25 | const mutations = {
26 | [SET_USER] (state, user) {
27 | state.user = user
28 | }
29 | }
30 |
31 | const getters = {
32 | user: state => {
33 | return state.user
34 | }
35 | }
36 |
37 | export default {
38 | state,
39 | actions,
40 | mutations,
41 | getters
42 | }
43 |
--------------------------------------------------------------------------------
/src/store/modules/nv.js:
--------------------------------------------------------------------------------
1 | import Vue from 'vue'
2 | import moment from 'moment'
3 |
4 | import api from '../api'
5 | import {
6 | SET_RESULT_INDEX,
7 | SET_THEME,
8 | SET_NOTES,
9 | SET_ACTIVE_NOTE,
10 | SET_ACTIVE_KEY,
11 | UPDATE_NOTE,
12 | CREATE_NOTE,
13 | DELETE_NOTE,
14 | TOGGLE_IS_PUBLIC } from '../constants'
15 |
16 | const state = {
17 | theme: 'light',
18 | notes: [],
19 | activeNote: null,
20 | activeKey: null
21 | }
22 |
23 | const actions = {
24 | INIT_NOTES: ({ state, commit, rootState }) => {
25 | return api.initNotesForUserId(rootState.auth.user.uid)
26 | },
27 |
28 | FETCH_USER_DATA: ({ state, commit, rootState }) => {
29 | return api.getDataForUserId(rootState.auth.user.uid)
30 | .then((res) => {
31 | commit(SET_NOTES, res.notes)
32 | commit(SET_THEME, res.theme)
33 | })
34 | },
35 |
36 | FETCH_PREVIEW_DATA: ({ state, commit, rootState }) => {
37 | return api.getPreviewData()
38 | .then((res) => {
39 | commit(SET_NOTES, res.notes)
40 | commit(SET_THEME, res.theme)
41 | })
42 | },
43 |
44 | CREATE_NOTE: ({ state, commit, rootState }, note) => {
45 | if (rootState.auth.user) {
46 | return api.createNote(rootState.auth.user.uid, note)
47 | .then((key) => {
48 | commit(SET_RESULT_INDEX, 0)
49 | commit(CREATE_NOTE, { key: key, note: note })
50 | })
51 | } else {
52 | commit(SET_RESULT_INDEX, 0)
53 | commit(CREATE_NOTE, { key: note.id, note: note })
54 | }
55 | },
56 |
57 | UPDATE_NOTE: ({ state, commit, rootState }) => {
58 | if (rootState.auth.user) {
59 | return api.updateNote(rootState.auth.user.uid, state.activeKey, state.activeNote)
60 | .then((res) => commit(UPDATE_NOTE, { key: res.key, date_modified: res.date_modified }))
61 | } else {
62 | const dateModified = moment().toString()
63 | commit(UPDATE_NOTE, { key: state.activeKey, date_modified: dateModified })
64 | }
65 | },
66 |
67 | DELETE_NOTE: ({ state, commit, rootState }) => {
68 | if (rootState.auth.user) {
69 | return api.deleteNote(rootState.auth.user.uid, state.activeKey)
70 | .then(() => { commit(DELETE_NOTE, state.activeKey)} )
71 | } else {
72 | commit(DELETE_NOTE, state.activeKey)
73 | }
74 | },
75 |
76 | UPDATE_THEME: ({ state, commit, rootState }, theme) => {
77 | if (rootState.auth.user) {
78 | return api.updateTheme(rootState.auth.user.uid, theme)
79 | .then((theme) => commit(SET_THEME, theme))
80 | } else {
81 | commit(SET_THEME, theme)
82 | }
83 | },
84 |
85 | TOGGLE_IS_PUBLIC: ({ state, commit, rootState }, note) => {
86 | return api.toggleIsPublic(rootState.auth.user.uid, state.activeKey, note)
87 | .then((res) => commit(TOGGLE_IS_PUBLIC, { key: res.key, is_public: res.is_public }))
88 | },
89 |
90 | FETCH_PUBLIC_NOTE_FOR_ID: ({ state, commit, rootState }, noteId) => {
91 | return api.getPublicNoteForId(noteId)
92 | .then((note) => commit(SET_ACTIVE_NOTE, note))
93 | }
94 | }
95 |
96 | const mutations = {
97 | [SET_THEME] (state, theme) {
98 | state.theme = theme
99 | },
100 |
101 | [SET_NOTES] (state, notes) {
102 | state.notes = notes
103 | },
104 |
105 | [SET_ACTIVE_NOTE] (state, note) {
106 | state.activeNote = note
107 | },
108 |
109 | [SET_ACTIVE_KEY] (state, key) {
110 | state.activeKey = key
111 | },
112 |
113 | [CREATE_NOTE] (state, data) {
114 | Vue.set(state.notes, data.key, data.note)
115 | state.activeKey = data.key
116 | state.activeNote = data.note
117 | },
118 |
119 | [UPDATE_NOTE] (state, data) {
120 | let note = state.notes[`${data.key}`]
121 | note.date_modified = data.date_modified
122 | },
123 |
124 | [DELETE_NOTE] (state, key) {
125 | Vue.delete(state.notes, key)
126 | },
127 |
128 | [TOGGLE_IS_PUBLIC] (state, data) {
129 | let note = state.notes[`${data.key}`]
130 | note.is_public = data.is_public
131 | }
132 | }
133 |
134 | const getters = {
135 | theme: state => {
136 | return state.theme
137 | },
138 |
139 | notes: state => {
140 | return state.notes
141 | },
142 |
143 | activeNote: state => {
144 | return state.activeNote
145 | },
146 |
147 | activeKey: state => {
148 | return state.activeKey
149 | }
150 | }
151 |
152 | export default {
153 | state,
154 | actions,
155 | mutations,
156 | getters
157 | }
158 |
--------------------------------------------------------------------------------
/webpack.config.js:
--------------------------------------------------------------------------------
1 | const path = require('path')
2 | const webpack = require('webpack')
3 | const ExtractTextPlugin = require('extract-text-webpack-plugin')
4 | const CopyWebpackPlugin = require('copy-webpack-plugin')
5 |
6 | module.exports = {
7 | entry: './src/app.js',
8 | output: {
9 | path: path.resolve(__dirname, './dist'),
10 | publicPath: "/",
11 | filename: 'build.js'
12 | },
13 | module: {
14 | rules: [
15 | {
16 | test: /\.vue$/,
17 | loader: 'vue-loader',
18 | options: {
19 | extractCSS: true,
20 | loaders: {
21 | 'scss': 'vue-style-loader!css-loader!sass-loader'
22 | }
23 | }
24 | },
25 | {
26 | test: /\.js$/,
27 | loader: 'babel-loader',
28 | exclude: /node_modules/
29 | },
30 | {
31 | test: /\.scss$/,
32 | loader: process.env.NODE_ENV !== 'production' ? 'style-loader!css-loader!sass-loader' : ExtractTextPlugin.extract('css-loader!sass-loader')
33 | },
34 | {
35 | test: /\.css$/,
36 | loader: 'style-loader!css-loader'
37 | },
38 | {
39 | test: /\.(png|jpg|gif|svg)$/,
40 | loader: 'file-loader',
41 | options: {
42 | context: path.resolve(__dirname, './src/'),
43 | name: '[path][name].[ext]?[hash]'
44 | }
45 | }
46 | ]
47 | },
48 | resolve: {
49 | alias: {
50 | 'vue$': 'vue/dist/vue.common.js'
51 | }
52 | },
53 | plugins: [
54 | new ExtractTextPlugin({filename: 'styles.css', disable: process.env.NODE_ENV !== 'production'})
55 | ],
56 | devServer: {
57 | historyApiFallback: true,
58 | noInfo: true
59 | },
60 | performance: {
61 | hints: false
62 | },
63 | devtool: '#eval-source-map'
64 | }
65 |
66 | if (process.env.NODE_ENV === 'production') {
67 | module.exports.devtool = '#source-map'
68 | // http://vue-loader.vuejs.org/en/workflow/production.html
69 | module.exports.plugins = (module.exports.plugins || []).concat([
70 | new webpack.DefinePlugin({
71 | 'process.env': {
72 | NODE_ENV: '"production"'
73 | }
74 | }),
75 | new webpack.optimize.UglifyJsPlugin({
76 | sourceMap: true,
77 | compress: {
78 | warnings: false
79 | }
80 | }),
81 | new webpack.LoaderOptionsPlugin({
82 | minimize: true
83 | }),
84 | new CopyWebpackPlugin([
85 | {from: './index.html'}
86 | ])
87 | ])
88 | }
89 |
--------------------------------------------------------------------------------