├── functions ├── .gitignore ├── package.json ├── index.js └── package-lock.json ├── .firebaserc ├── static ├── robots.txt ├── icon.png ├── avatar.jpg ├── favicon.ico └── README.md ├── plugins ├── vue-instantsearch.js ├── to-date.js ├── README.md ├── firebase.js └── click-outside.js ├── firebase.json ├── middleware ├── anonymous-access.js ├── authenticated-access.js └── README.md ├── serverMiddleware ├── firebase-admin.js └── sitemap.js ├── services └── date.js ├── assets ├── css │ ├── tailwind.css │ └── styles.css └── README.md ├── components ├── README.md ├── icons │ ├── IconRemove.vue │ ├── IconFormatQuote.vue │ ├── IconFormatItalic.vue │ ├── IconFormatStrikethrough.vue │ ├── IconClose.vue │ ├── IconFormatCode.vue │ ├── IconFormatUnderline.vue │ ├── IconRedo.vue │ ├── IconUndo.vue │ ├── IconFormatListNumbered.vue │ ├── IconLink.vue │ ├── IconFormatBold.vue │ ├── IconAddPhoto.vue │ ├── IconFormatListBulleted.vue │ └── IconLinkOff.vue ├── Logo.vue ├── LoadingSpinner.vue ├── Teaser.vue ├── BlogDetails.vue ├── Editor.vue └── BlogForm.vue ├── .editorconfig ├── layouts ├── README.md ├── admin.vue └── default.vue ├── pages ├── README.md ├── logout.vue ├── new.vue ├── blog │ └── _id │ │ ├── edit.vue │ │ ├── preview.vue │ │ └── index.vue ├── login.vue ├── admin.vue └── index.vue ├── tailwind.config.js ├── store ├── README.md └── index.js ├── app.yaml ├── .gcloudignore ├── README.md ├── package.json ├── .gitignore ├── nuxt.config.js └── .eslintrc.js /functions/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ -------------------------------------------------------------------------------- /.firebaserc: -------------------------------------------------------------------------------- 1 | { 2 | "projects": { 3 | "default": "donlalicon" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /static/robots.txt: -------------------------------------------------------------------------------- 1 | User-agent: * 2 | Sitemap: https://donlalicon.dev/sitemap.xml 3 | -------------------------------------------------------------------------------- /static/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/angheloko/donlalicon/HEAD/static/icon.png -------------------------------------------------------------------------------- /static/avatar.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/angheloko/donlalicon/HEAD/static/avatar.jpg -------------------------------------------------------------------------------- /static/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/angheloko/donlalicon/HEAD/static/favicon.ico -------------------------------------------------------------------------------- /plugins/vue-instantsearch.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import InstantSearch from 'vue-instantsearch' 3 | 4 | Vue.use(InstantSearch) 5 | -------------------------------------------------------------------------------- /firebase.json: -------------------------------------------------------------------------------- 1 | { 2 | "functions": { 3 | "predeploy": [ 4 | "npm --prefix \"$RESOURCE_DIR\" run lint" 5 | ] 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /middleware/anonymous-access.js: -------------------------------------------------------------------------------- 1 | export default function ({ store, redirect }) { 2 | if (store.state.auth) { 3 | return redirect('/') 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /middleware/authenticated-access.js: -------------------------------------------------------------------------------- 1 | export default function ({ store, redirect }) { 2 | if (!store.state.auth) { 3 | return redirect('/login') 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /plugins/to-date.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import date from '~/services/date' 3 | 4 | Vue.filter('toDate', (value) => { 5 | return date.toDate(value) 6 | }) 7 | -------------------------------------------------------------------------------- /serverMiddleware/firebase-admin.js: -------------------------------------------------------------------------------- 1 | const admin = require('firebase-admin') 2 | module.exports = admin.initializeApp({ 3 | credential: admin.credential.applicationDefault() 4 | }) 5 | -------------------------------------------------------------------------------- /services/date.js: -------------------------------------------------------------------------------- 1 | import moment from 'moment' 2 | 3 | export default { 4 | toDate (value) { 5 | if (!value) { 6 | return '' 7 | } 8 | return moment.unix(value.seconds).format('D MMM YYYY') 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /assets/css/tailwind.css: -------------------------------------------------------------------------------- 1 | /*noinspection CssUnknownTarget*/ 2 | @import 'tailwindcss/base'; 3 | /*noinspection CssUnknownTarget*/ 4 | @import 'tailwindcss/components'; 5 | /*noinspection CssUnknownTarget*/ 6 | @import 'tailwindcss/utilities'; 7 | -------------------------------------------------------------------------------- /components/README.md: -------------------------------------------------------------------------------- 1 | # COMPONENTS 2 | 3 | **This directory is not required, you can delete it if you don't want to use it.** 4 | 5 | The components directory contains your Vue.js Components. 6 | 7 | _Nuxt.js doesn't supercharge these components._ 8 | -------------------------------------------------------------------------------- /components/icons/IconRemove.vue: -------------------------------------------------------------------------------- 1 | 6 | 11 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # editorconfig.org 2 | root = true 3 | 4 | [*] 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | charset = utf-8 9 | trim_trailing_whitespace = true 10 | insert_final_newline = true 11 | 12 | [*.md] 13 | trim_trailing_whitespace = false 14 | -------------------------------------------------------------------------------- /components/icons/IconFormatQuote.vue: -------------------------------------------------------------------------------- 1 | 6 | 11 | -------------------------------------------------------------------------------- /layouts/README.md: -------------------------------------------------------------------------------- 1 | # LAYOUTS 2 | 3 | **This directory is not required, you can delete it if you don't want to use it.** 4 | 5 | This directory contains your Application Layouts. 6 | 7 | More information about the usage of this directory in [the documentation](https://nuxtjs.org/guide/views#layouts). 8 | -------------------------------------------------------------------------------- /components/icons/IconFormatItalic.vue: -------------------------------------------------------------------------------- 1 | 6 | 11 | -------------------------------------------------------------------------------- /pages/README.md: -------------------------------------------------------------------------------- 1 | # PAGES 2 | 3 | This directory contains your Application Views and Routes. 4 | The framework reads all the `*.vue` files inside this directory and creates the router of your application. 5 | 6 | More information about the usage of this directory in [the documentation](https://nuxtjs.org/guide/routing). 7 | -------------------------------------------------------------------------------- /components/icons/IconFormatStrikethrough.vue: -------------------------------------------------------------------------------- 1 | 6 | 11 | -------------------------------------------------------------------------------- /assets/README.md: -------------------------------------------------------------------------------- 1 | # ASSETS 2 | 3 | **This directory is not required, you can delete it if you don't want to use it.** 4 | 5 | This directory contains your un-compiled assets such as LESS, SASS, or JavaScript. 6 | 7 | More information about the usage of this directory in [the documentation](https://nuxtjs.org/guide/assets#webpacked). 8 | -------------------------------------------------------------------------------- /pages/logout.vue: -------------------------------------------------------------------------------- 1 | 4 | 15 | 17 | -------------------------------------------------------------------------------- /components/icons/IconClose.vue: -------------------------------------------------------------------------------- 1 | 6 | 11 | -------------------------------------------------------------------------------- /components/icons/IconFormatCode.vue: -------------------------------------------------------------------------------- 1 | 6 | 11 | -------------------------------------------------------------------------------- /plugins/README.md: -------------------------------------------------------------------------------- 1 | # PLUGINS 2 | 3 | **This directory is not required, you can delete it if you don't want to use it.** 4 | 5 | This directory contains Javascript plugins that you want to run before mounting the root Vue.js application. 6 | 7 | More information about the usage of this directory in [the documentation](https://nuxtjs.org/guide/plugins). 8 | -------------------------------------------------------------------------------- /components/icons/IconFormatUnderline.vue: -------------------------------------------------------------------------------- 1 | 6 | 11 | -------------------------------------------------------------------------------- /components/icons/IconRedo.vue: -------------------------------------------------------------------------------- 1 | 6 | 11 | -------------------------------------------------------------------------------- /components/icons/IconUndo.vue: -------------------------------------------------------------------------------- 1 | 6 | 11 | -------------------------------------------------------------------------------- /components/icons/IconFormatListNumbered.vue: -------------------------------------------------------------------------------- 1 | 6 | 11 | -------------------------------------------------------------------------------- /tailwind.config.js: -------------------------------------------------------------------------------- 1 | /* 2 | ** TailwindCSS Configuration File 3 | ** 4 | ** Docs: https://tailwindcss.com/docs/configuration 5 | ** Default: https://github.com/tailwindcss/tailwindcss/blob/master/stubs/defaultConfig.stub.js 6 | */ 7 | module.exports = { 8 | theme: {}, 9 | variants: { 10 | margin: ['responsive', 'first'] 11 | }, 12 | plugins: [], 13 | purge: [ 14 | './**/*.vue' 15 | ] 16 | } 17 | -------------------------------------------------------------------------------- /middleware/README.md: -------------------------------------------------------------------------------- 1 | # MIDDLEWARE 2 | 3 | **This directory is not required, you can delete it if you don't want to use it.** 4 | 5 | This directory contains your application middleware. 6 | Middleware let you define custom functions that can be run before rendering either a page or a group of pages. 7 | 8 | More information about the usage of this directory in [the documentation](https://nuxtjs.org/guide/routing#middleware). 9 | -------------------------------------------------------------------------------- /pages/new.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 20 | 21 | 23 | -------------------------------------------------------------------------------- /components/icons/IconLink.vue: -------------------------------------------------------------------------------- 1 | 6 | 11 | -------------------------------------------------------------------------------- /store/README.md: -------------------------------------------------------------------------------- 1 | # STORE 2 | 3 | **This directory is not required, you can delete it if you don't want to use it.** 4 | 5 | This directory contains your Vuex Store files. 6 | Vuex Store option is implemented in the Nuxt.js framework. 7 | 8 | Creating a file in this directory automatically activates the option in the framework. 9 | 10 | More information about the usage of this directory in [the documentation](https://nuxtjs.org/guide/vuex-store). 11 | -------------------------------------------------------------------------------- /components/icons/IconFormatBold.vue: -------------------------------------------------------------------------------- 1 | 6 | 11 | -------------------------------------------------------------------------------- /components/icons/IconAddPhoto.vue: -------------------------------------------------------------------------------- 1 | 6 | 11 | -------------------------------------------------------------------------------- /static/README.md: -------------------------------------------------------------------------------- 1 | # STATIC 2 | 3 | **This directory is not required, you can delete it if you don't want to use it.** 4 | 5 | This directory contains your static files. 6 | Each file inside this directory is mapped to `/`. 7 | Thus you'd want to delete this README.md before deploying to production. 8 | 9 | Example: `/static/robots.txt` is mapped as `/robots.txt`. 10 | 11 | More information about the usage of this directory in [the documentation](https://nuxtjs.org/guide/assets#static). 12 | -------------------------------------------------------------------------------- /layouts/admin.vue: -------------------------------------------------------------------------------- 1 | 15 | 17 | -------------------------------------------------------------------------------- /components/icons/IconFormatListBulleted.vue: -------------------------------------------------------------------------------- 1 | 6 | 11 | -------------------------------------------------------------------------------- /app.yaml: -------------------------------------------------------------------------------- 1 | runtime: nodejs10 2 | 3 | instance_class: F2 4 | 5 | default_expiration: "8h" 6 | 7 | handlers: 8 | - url: /_nuxt 9 | static_dir: .nuxt/dist/client 10 | secure: always 11 | 12 | - url: /(.*\.(gif|png|jpg|ico|txt))$ 13 | static_files: static/\1 14 | upload: static/.*\.(gif|png|jpg|ico|txt)$ 15 | secure: always 16 | 17 | - url: /.* 18 | script: auto 19 | secure: always 20 | 21 | automatic_scaling: 22 | min_instances: 1 23 | 24 | env_variables: 25 | HOST: '0.0.0.0' 26 | NODE_ENV: 'production' 27 | -------------------------------------------------------------------------------- /.gcloudignore: -------------------------------------------------------------------------------- 1 | # This file specifies files that are *not* uploaded to Google Cloud Platform 2 | # using gcloud. It follows the same syntax as .gitignore, with the addition of 3 | # "#!include" directives (which insert the entries of the given .gitignore-style 4 | # file at that point). 5 | # 6 | # For more information, run: 7 | # $ gcloud topic gcloudignore 8 | # 9 | .gcloudignore 10 | # If you would like to upload your .git directory, .gitignore file or files 11 | # from your .gitignore file, remove the corresponding line 12 | # below: 13 | .git 14 | .gitignore 15 | 16 | # Node.js dependencies: 17 | node_modules/ -------------------------------------------------------------------------------- /components/icons/IconLinkOff.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 12 | 13 | 16 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # donlalicon.dev 2 | 3 | [donlalicon.dev](https://donlalicon.dev) is now a statically generated site. You can check [angheloko/donlalicon-static](https://github.com/angheloko/donlalicon-static) for the new source code. 4 | 5 | My blog 6 | 7 | ## Build Setup 8 | 9 | ``` bash 10 | # install dependencies 11 | $ npm run install 12 | 13 | # serve with hot reload at localhost:3000 14 | $ npm run dev 15 | 16 | # build for production and launch server 17 | $ npm run build 18 | $ npm run start 19 | 20 | # generate static project 21 | $ npm run generate 22 | ``` 23 | 24 | For detailed explanation on how things work, check out [Nuxt.js docs](https://nuxtjs.org). 25 | -------------------------------------------------------------------------------- /functions/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "functions", 3 | "description": "Cloud Functions for Firebase", 4 | "scripts": { 5 | "lint": "eslint .", 6 | "serve": "firebase emulators:start --only functions", 7 | "shell": "firebase functions:shell", 8 | "start": "npm run shell", 9 | "deploy": "firebase deploy --only functions", 10 | "logs": "firebase functions:log" 11 | }, 12 | "engines": { 13 | "node": "10" 14 | }, 15 | "dependencies": { 16 | "algoliasearch": "^4.3.0", 17 | "firebase-admin": "^8.12.1", 18 | "firebase-functions": "^3.7.0", 19 | "stopword": "^1.0.0" 20 | }, 21 | "devDependencies": { 22 | "eslint": "^6.1.0", 23 | "eslint-plugin-promise": "^4.2.1" 24 | }, 25 | "private": true 26 | } 27 | -------------------------------------------------------------------------------- /plugins/firebase.js: -------------------------------------------------------------------------------- 1 | import firebase from 'firebase/app' 2 | import 'firebase/firestore' 3 | import 'firebase/auth' 4 | import 'firebase/analytics' 5 | import 'firebase/storage' 6 | 7 | export default ({ env, store }, inject) => { 8 | const firebaseConfig = { 9 | apiKey: env.FB_API_KEY, 10 | authDomain: env.FB_AUTH_DOMAIN, 11 | databaseURL: env.FB_DB_URL, 12 | projectId: env.FB_PROJECT_ID, 13 | storageBucket: env.FB_STORAGE_BUCKET, 14 | messagingSenderId: env.FB_MESSAGING_SENDER_ID, 15 | appId: env.FB_APP_ID, 16 | measurementId: env.FB_MEASUREMENT_ID 17 | } 18 | 19 | if (!firebase.apps.length) { 20 | // Initialize Firebase 21 | firebase.initializeApp(firebaseConfig) 22 | } 23 | 24 | if (process.client) { 25 | firebase.analytics() 26 | firebase.auth().onAuthStateChanged((user) => { 27 | store.dispatch('setAuth', user) 28 | }) 29 | } 30 | 31 | inject('firebase', firebase) 32 | } 33 | -------------------------------------------------------------------------------- /components/Logo.vue: -------------------------------------------------------------------------------- 1 | 20 | 34 | -------------------------------------------------------------------------------- /serverMiddleware/sitemap.js: -------------------------------------------------------------------------------- 1 | const { createGzip } = require('zlib') 2 | const { SitemapStream, streamToPromise } = require('sitemap') 3 | const app = require('./firebase-admin') 4 | 5 | export default function (req, res, next) { 6 | const db = app.firestore() 7 | const smStream = new SitemapStream({ hostname: 'https://donlalicon.dev/' }) 8 | const pipeline = smStream.pipe(createGzip()) 9 | 10 | db.collection('blogs') 11 | .where('published', '==', true) 12 | .orderBy('created', 'desc') 13 | .get() 14 | .then((querySnapshot) => { 15 | for (const doc of querySnapshot.docs) { 16 | const data = doc.data() 17 | smStream.write({ 18 | url: `/blog/${doc.id}`, 19 | lastmod: data.changed 20 | .toDate() 21 | .toISOString() 22 | }) 23 | } 24 | 25 | smStream.end() 26 | 27 | streamToPromise(pipeline).then((buffer) => { 28 | res.writeHead(200, { 29 | 'Content-Type': 'application/xml', 30 | 'Content-Encoding': 'gzip' 31 | }) 32 | res.end(buffer) 33 | }) 34 | }) 35 | } 36 | -------------------------------------------------------------------------------- /components/LoadingSpinner.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 14 | 15 | 53 | -------------------------------------------------------------------------------- /plugins/click-outside.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | 3 | Vue.directive('click-outside', { 4 | bind (el, binding, vNode) { 5 | // Provided expression must evaluate to a function. 6 | if (typeof binding.value !== 'function') { 7 | const compName = vNode.context.name 8 | // prettier-ignore 9 | let warn = `[Vue-click-outside:] provided expression '${binding.expression}' is not a function, but has to be` 10 | if (compName) { 11 | warn += `Found in component '${compName}'` 12 | } 13 | // eslint-disable-next-line no-console 14 | console.warn(warn) 15 | } 16 | // Define Handler and cache it on the element 17 | const bubble = binding.modifiers.bubble 18 | const handler = (e) => { 19 | if (bubble || (!el.contains(e.target) && el !== e.target)) { 20 | binding.value(e) 21 | } 22 | } 23 | el.__vueClickOutside__ = handler 24 | 25 | // add Event Listeners 26 | document.addEventListener('click', handler) 27 | }, 28 | 29 | unbind (el, binding) { 30 | // Remove Event Listeners 31 | document.removeEventListener('click', el.__vueClickOutside__) 32 | el.__vueClickOutside__ = null 33 | } 34 | }) 35 | -------------------------------------------------------------------------------- /pages/blog/_id/edit.vue: -------------------------------------------------------------------------------- 1 | 4 | 46 | 48 | -------------------------------------------------------------------------------- /pages/blog/_id/preview.vue: -------------------------------------------------------------------------------- 1 | 4 | 45 | 47 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "donlalicon.dev", 3 | "version": "1.0.0", 4 | "description": "Personal blog of Don Lalicon.", 5 | "author": "Don Lalicon", 6 | "private": true, 7 | "scripts": { 8 | "dev": "nuxt", 9 | "build": "nuxt build", 10 | "start": "nuxt start", 11 | "generate": "nuxt generate", 12 | "lint": "eslint --ext .js,.vue --ignore-path .gitignore .", 13 | "deploy": "npm run build && gcloud app deploy --project donlalicon" 14 | }, 15 | "dependencies": { 16 | "@nuxtjs/dotenv": "^1.4.0", 17 | "@nuxtjs/pwa": "^3.0.0-beta.20", 18 | "algoliasearch": "^4.3.0", 19 | "cookieparser": "^0.1.0", 20 | "firebase": "^7.15.3", 21 | "firebase-admin": "^8.12.1", 22 | "highlight.js": "^9.18.1", 23 | "lodash": "^4.17.15", 24 | "moment": "^2.27.0", 25 | "nuxt": "^2.13.0", 26 | "sitemap": "^6.1.5", 27 | "tiptap": "^1.27.1", 28 | "tiptap-extensions": "^1.29.1", 29 | "vue-instantsearch": "^2.7.1" 30 | }, 31 | "devDependencies": { 32 | "@nuxtjs/eslint-config": "^2.0.0", 33 | "@nuxtjs/eslint-module": "^1.0.0", 34 | "@nuxtjs/tailwindcss": "^1.0.0", 35 | "babel-eslint": "^10.0.1", 36 | "eslint": "^6.1.0", 37 | "eslint-plugin-nuxt": "^1.0.0", 38 | "eslint-plugin-promise": "^4.2.1" 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /store/index.js: -------------------------------------------------------------------------------- 1 | const cookieParser = process.server ? require('cookieparser') : undefined 2 | 3 | export const state = () => { 4 | return { 5 | auth: null 6 | } 7 | } 8 | export const getters = { 9 | auth: (state) => { 10 | return state.auth 11 | } 12 | } 13 | export const mutations = { 14 | setAuth (state, auth) { 15 | state.auth = auth 16 | } 17 | } 18 | export const actions = { 19 | nuxtServerInit ({ commit }, { req }) { 20 | let token = null 21 | if (req.headers.cookie) { 22 | const parsed = cookieParser.parse(req.headers.cookie) 23 | token = parsed.token 24 | } 25 | commit('setAuth', token) 26 | }, 27 | setAuth ({ commit }, user) { 28 | if (!user) { 29 | commit('setAuth', null) 30 | document.cookie = 'token=;path=/;expires=0' 31 | } else { 32 | user.getIdToken().then((token) => { 33 | commit('setAuth', token) 34 | const expiresIn = 60 * 60 * 24 * 5 * 1000 // 5 days. 35 | document.cookie = 'token=' + token + ';path=/;max-age=' + expiresIn 36 | return null 37 | }).catch((error) => { 38 | // eslint-disable-next-line no-console 39 | console.error('Error getting ID token.', error) 40 | commit('setAuth', null) 41 | document.cookie = 'token=;path=/;expires=0' 42 | }) 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /components/Teaser.vue: -------------------------------------------------------------------------------- 1 | 34 | 35 | 51 | 52 | 54 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by .ignore support plugin (hsz.mobi) 2 | ### Node template 3 | # Logs 4 | /logs 5 | *.log 6 | npm-debug.log* 7 | yarn-debug.log* 8 | yarn-error.log* 9 | 10 | # Runtime data 11 | pids 12 | *.pid 13 | *.seed 14 | *.pid.lock 15 | 16 | # Directory for instrumented libs generated by jscoverage/JSCover 17 | lib-cov 18 | 19 | # Coverage directory used by tools like istanbul 20 | coverage 21 | 22 | # nyc test coverage 23 | .nyc_output 24 | 25 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 26 | .grunt 27 | 28 | # Bower dependency directory (https://bower.io/) 29 | bower_components 30 | 31 | # node-waf configuration 32 | .lock-wscript 33 | 34 | # Compiled binary addons (https://nodejs.org/api/addons.html) 35 | build/Release 36 | 37 | # Dependency directories 38 | node_modules/ 39 | jspm_packages/ 40 | 41 | # TypeScript v1 declaration files 42 | typings/ 43 | 44 | # Optional npm cache directory 45 | .npm 46 | 47 | # Optional eslint cache 48 | .eslintcache 49 | 50 | # Optional REPL history 51 | .node_repl_history 52 | 53 | # Output of 'npm pack' 54 | *.tgz 55 | 56 | # Yarn Integrity file 57 | .yarn-integrity 58 | 59 | # dotenv environment variables file 60 | .env 61 | 62 | # parcel-bundler cache (https://parceljs.org/) 63 | .cache 64 | 65 | # next.js build output 66 | .next 67 | 68 | # nuxt.js build output 69 | .nuxt 70 | 71 | # Nuxt generate 72 | dist 73 | 74 | # vuepress build output 75 | .vuepress/dist 76 | 77 | # Serverless directories 78 | .serverless 79 | 80 | # IDE / Editor 81 | .idea 82 | 83 | # Service worker 84 | sw.* 85 | 86 | # Mac OSX 87 | .DS_Store 88 | 89 | # Vim swap files 90 | *.swp 91 | /keys/ 92 | /static/sitemap.xml 93 | -------------------------------------------------------------------------------- /pages/login.vue: -------------------------------------------------------------------------------- 1 | 40 | 41 | 76 | 77 | 79 | -------------------------------------------------------------------------------- /functions/index.js: -------------------------------------------------------------------------------- 1 | const functions = require('firebase-functions') 2 | const admin = require('firebase-admin') 3 | const algoliasearch = require('algoliasearch') 4 | const stopword = require('stopword') 5 | 6 | admin.initializeApp() 7 | 8 | exports.indexBlog = functions.firestore.document('blogs/{blogId}') 9 | .onWrite((change, context) => { 10 | const document = change.after.exists ? change.after.data() : null 11 | const { blogId } = context.params 12 | 13 | const ALGOLIA_APP_ID = functions.config().algolia.app_id 14 | const ALGOLIA_API_KEY = functions.config().algolia.api_key 15 | const client = algoliasearch(ALGOLIA_APP_ID, ALGOLIA_API_KEY) 16 | const index = client.initIndex('blogs') 17 | 18 | function deleteObject () { 19 | return index 20 | .deleteObject(blogId) 21 | .then(() => { 22 | return true 23 | }) 24 | .catch((error) => { 25 | console.error('Error deleting blog from index', error) 26 | }) 27 | } 28 | 29 | function saveObject () { 30 | // The body property is stripped of HTMl tags and stop words. 31 | return index 32 | .saveObject({ 33 | objectID: blogId, 34 | title: document.title, 35 | body: stopword.removeStopwords(document.body.replace(/(<([^>]+)>)/ig, '').split(' ')).join(' ').replace(/\s\s+/g, ' '), 36 | tags: document.tags, 37 | changed: document.changed.toMillis() 38 | }) 39 | .then(() => { 40 | return true 41 | }) 42 | .catch((error) => { 43 | console.error('Error indexing blog', error) 44 | }) 45 | } 46 | 47 | if (!document) { 48 | return deleteObject(blogId) 49 | } else if (!document.published) { 50 | return deleteObject(blogId) 51 | } else { 52 | return saveObject() 53 | } 54 | }) 55 | -------------------------------------------------------------------------------- /pages/admin.vue: -------------------------------------------------------------------------------- 1 | 50 | 51 | 75 | 76 | 82 | -------------------------------------------------------------------------------- /assets/css/styles.css: -------------------------------------------------------------------------------- 1 | /*noinspection CssUnknownTarget*/ 2 | @import 'highlight.js/styles/solarized-dark'; 3 | 4 | /* purgecss start ignore */ 5 | h1 { 6 | @apply text-4xl font-serif; 7 | } 8 | 9 | h2 { 10 | @apply text-2xl font-serif; 11 | } 12 | 13 | h3 { 14 | @apply text-xl; 15 | } 16 | 17 | h4, h5, h6 { 18 | @apply text-base font-black; 19 | } 20 | 21 | a { 22 | @apply text-blue-500 underline; 23 | } 24 | 25 | a:hover { 26 | @apply text-blue-700; 27 | } 28 | 29 | p { 30 | @apply my-4; 31 | } 32 | 33 | ul, 34 | ol { 35 | @apply pl-8 my-4; 36 | } 37 | 38 | ul { 39 | list-style-type: disc; 40 | } 41 | 42 | ol { 43 | list-style-type: decimal; 44 | } 45 | 46 | li > p, 47 | li > ol, 48 | li > ul { 49 | margin: 0; 50 | } 51 | 52 | pre code { 53 | @apply hljs my-4; 54 | } 55 | 56 | p code { 57 | display: inline-block; 58 | @apply bg-gray-300 text-sm p-1 rounded-sm font-semibold; 59 | } 60 | 61 | blockquote { 62 | @apply border-l-4 border-gray-600 italic pl-4; 63 | } 64 | 65 | label { 66 | @apply block text-gray-600 text-sm font-bold mb-2; 67 | } 68 | 69 | textarea, 70 | input[type="text"], 71 | input[type="password"] { 72 | @apply shadow appearance-none border rounded w-full py-2 px-3 leading-tight; 73 | } 74 | 75 | textarea:focus, 76 | input[type="text"]:focus, 77 | input[type="password"]:focus { 78 | @apply outline-none border-blue-500; 79 | } 80 | 81 | button { 82 | @apply text-gray-600 font-bold py-2 px-4 rounded border-gray-500 border shadow; 83 | } 84 | 85 | .ProseMirror { 86 | outline: none; 87 | } 88 | 89 | .editor p.is-editor-empty:first-child::before { 90 | content: attr(data-empty-text); 91 | float: left; 92 | pointer-events: none; 93 | height: 0; 94 | font-style: italic; 95 | @apply text-gray-400; 96 | } 97 | /* purgecss end ignore */ 98 | 99 | .content { 100 | overflow-wrap: break-word; 101 | word-wrap: break-word; 102 | word-break: break-word; 103 | } 104 | -------------------------------------------------------------------------------- /nuxt.config.js: -------------------------------------------------------------------------------- 1 | 2 | export default { 3 | mode: 'universal', 4 | /* 5 | ** Headers of the page 6 | */ 7 | head: { 8 | title: 'Don Lalicon - Codesmith', 9 | titleTemplate: '%s - Don Lalicon - Codesmith', 10 | meta: [ 11 | { charset: 'utf-8' }, 12 | { name: 'viewport', content: 'width=device-width, initial-scale=1' }, 13 | { hid: 'description', name: 'description', content: 'Personal blog of Don Lalicon.' } 14 | ], 15 | link: [ 16 | { rel: 'icon', type: 'image/x-icon', href: '/favicon.ico' } 17 | ] 18 | }, 19 | /* 20 | ** Customize the progress-bar color 21 | */ 22 | loading: { color: '#dd6b20' }, 23 | /* 24 | ** Global CSS 25 | */ 26 | css: [ 27 | '~/assets/css/styles.css' 28 | ], 29 | purgeCSS: { 30 | whitelist: ['hljs'], 31 | whitelistPatterns: [/hljs-.+$/], 32 | whitelistPatternsChildren: [/hljs-.+$/] 33 | }, 34 | /* 35 | ** Plugins to load before mounting the App 36 | */ 37 | plugins: [ 38 | '~/plugins/firebase', 39 | '~/plugins/to-date', 40 | { 41 | src: '~/plugins/vue-instantsearch', 42 | mode: 'client' 43 | }, 44 | { 45 | src: '~/plugins/click-outside', 46 | mode: 'client' 47 | } 48 | ], 49 | /* 50 | ** Nuxt.js dev-modules 51 | */ 52 | buildModules: [ 53 | // Doc: https://github.com/nuxt-community/eslint-module 54 | '@nuxtjs/eslint-module', 55 | // Doc: https://github.com/nuxt-community/nuxt-tailwindcss 56 | '@nuxtjs/tailwindcss' 57 | ], 58 | /* 59 | ** Nuxt.js modules 60 | */ 61 | modules: [ 62 | '@nuxtjs/pwa', 63 | // Doc: https://github.com/nuxt-community/dotenv-module 64 | '@nuxtjs/dotenv' 65 | ], 66 | /* 67 | ** Build configuration 68 | */ 69 | build: { 70 | /* 71 | ** You can extend webpack config here 72 | */ 73 | extend (config, ctx) { 74 | } 75 | }, 76 | serverMiddleware: [ 77 | { 78 | path: '/sitemap.xml', 79 | handler: '~/serverMiddleware/sitemap.js' 80 | } 81 | ] 82 | } 83 | -------------------------------------------------------------------------------- /pages/index.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 85 | -------------------------------------------------------------------------------- /components/BlogDetails.vue: -------------------------------------------------------------------------------- 1 | 49 | 50 | 98 | 100 | -------------------------------------------------------------------------------- /pages/blog/_id/index.vue: -------------------------------------------------------------------------------- 1 | 4 | 149 | 151 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | env: { 4 | browser: true, 5 | node: true 6 | }, 7 | parserOptions: { 8 | parser: 'babel-eslint' 9 | }, 10 | extends: [ 11 | '@nuxtjs', 12 | 'plugin:nuxt/recommended', 13 | 'plugin:promise/recommended' 14 | ], 15 | // add your custom rules here 16 | rules: { 17 | 'vue/no-v-html': 'off', 18 | 19 | 'no-console': 'off', 20 | 21 | 'consistent-return': 'off', 22 | 23 | // Removed rule 'disallow multiple spaces in regular expressions' from recommended eslint rules 24 | 'no-regex-spaces': 'off', 25 | 26 | // Removed rule 'disallow the use of debugger' from recommended eslint rules 27 | 'no-debugger': 'off', 28 | 29 | // Removed rule 'disallow unused variables' from recommended eslint rules 30 | 'no-unused-vars': 'off', 31 | 32 | // Removed rule 'disallow mixed spaces and tabs for indentation' from recommended eslint rules 33 | 'no-mixed-spaces-and-tabs': 'off', 34 | 35 | // Removed rule 'disallow the use of undeclared variables unless mentioned in /*global */ comments' from recommended eslint rules 36 | 'no-undef': 'off', 37 | 38 | // Warn against template literal placeholder syntax in regular strings 39 | 'no-template-curly-in-string': 1, 40 | 41 | // Warn if no return statements in callbacks of array methods 42 | 'array-callback-return': 1, 43 | 44 | // Require the use of === and !== 45 | 'eqeqeq': 2, 46 | 47 | // Disallow the use of alert, confirm, and prompt 48 | 'no-alert': 2, 49 | 50 | // Disallow the use of arguments.caller or arguments.callee 51 | 'no-caller': 2, 52 | 53 | // Disallow null comparisons without type-checking operators 54 | 'no-eq-null': 2, 55 | 56 | // Disallow the use of eval() 57 | 'no-eval': 2, 58 | 59 | // Warn against extending native types 60 | 'no-extend-native': 1, 61 | 62 | // Warn against unnecessary calls to .bind() 63 | 'no-extra-bind': 1, 64 | 65 | // Warn against unnecessary labels 66 | 'no-extra-label': 1, 67 | 68 | // Disallow leading or trailing decimal points in numeric literals 69 | 'no-floating-decimal': 2, 70 | 71 | // Warn against shorthand type conversions 72 | 'no-implicit-coercion': 1, 73 | 74 | // Warn against function declarations and expressions inside loop statements 75 | 'no-loop-func': 1, 76 | 77 | // Disallow new operators with the Function object 78 | 'no-new-func': 2, 79 | 80 | // Warn against new operators with the String, Number, and Boolean objects 81 | 'no-new-wrappers': 1, 82 | 83 | // Disallow throwing literals as exceptions 84 | 'no-throw-literal': 2, 85 | 86 | // Require using Error objects as Promise rejection reasons 87 | 'prefer-promise-reject-errors': 2, 88 | 89 | // Enforce “for” loop update clause moving the counter in the right direction 90 | 'for-direction': 2, 91 | 92 | // Enforce return statements in getters 93 | 'getter-return': 2, 94 | 95 | // Disallow await inside of loops 96 | 'no-await-in-loop': 2, 97 | 98 | // Disallow comparing against -0 99 | 'no-compare-neg-zero': 2, 100 | 101 | // Warn against catch clause parameters from shadowing variables in the outer scope 102 | 'no-catch-shadow': 1, 103 | 104 | // Disallow identifiers from shadowing restricted names 105 | 'no-shadow-restricted-names': 2, 106 | 107 | // Enforce return statements in callbacks of array methods 108 | 'callback-return': 2, 109 | 110 | // Require error handling in callbacks 111 | 'handle-callback-err': 2, 112 | 113 | // Warn against string concatenation with __dirname and __filename 114 | 'no-path-concat': 1, 115 | 116 | // Prefer using arrow functions for callbacks 117 | 'prefer-arrow-callback': 1, 118 | 119 | // Return inside each then() to create readable and reusable Promise chains. 120 | // Forces developers to return console logs and http calls in promises. 121 | 'promise/always-return': 2, 122 | 123 | //Enforces the use of catch() on un-returned promises 124 | 'promise/catch-or-return': 2, 125 | 126 | // Warn against nested then() or catch() statements 127 | 'promise/no-nesting': 1 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /layouts/default.vue: -------------------------------------------------------------------------------- 1 | 82 | 116 | -------------------------------------------------------------------------------- /components/Editor.vue: -------------------------------------------------------------------------------- 1 | 143 | 144 | 299 | 300 | 360 | -------------------------------------------------------------------------------- /components/BlogForm.vue: -------------------------------------------------------------------------------- 1 |