Initial commit

This commit is contained in:
2021-03-09 22:00:10 +01:00
commit 70c0886aa5
52 changed files with 12763 additions and 0 deletions

3
.browserslistrc Normal file
View File

@@ -0,0 +1,3 @@
> 1%
last 2 versions
not dead

33
.eslintrc.js Normal file
View File

@@ -0,0 +1,33 @@
module.exports = {
root: true,
env: {
node: true
},
extends: [
'plugin:vue/vue3-essential',
'eslint:recommended',
'@vue/typescript/recommended',
'@vue/prettier',
'@vue/prettier/@typescript-eslint'
],
parserOptions: {
ecmaVersion: 2020
},
rules: {
'no-console': process.env.NODE_ENV === 'production' ? 'warn' : 'off',
'no-debugger': process.env.NODE_ENV === 'production' ? 'warn' : 'off',
'@typescript-eslint/explicit-module-boundary-types': 'off',
'@typescript-eslint/camelcase': 'off'
},
overrides: [
{
files: [
'**/__tests__/*.{j,t}s?(x)',
'**/tests/unit/**/*.spec.{j,t}s?(x)'
],
env: {
jest: true
}
}
]
}

23
.gitignore vendored Normal file
View File

@@ -0,0 +1,23 @@
.DS_Store
node_modules
/dist
# local env files
.env.local
.env.*.local
# Log files
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
# Editor directories and files
.idea
.vscode
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?

1
.node-version Normal file
View File

@@ -0,0 +1 @@
v12.17.0

6
.prettierrc Normal file
View File

@@ -0,0 +1,6 @@
{
"semi": false,
"singleQuote": true,
"trailingComma": "none",
"arrowParens": "always"
}

56
README.md Normal file
View File

@@ -0,0 +1,56 @@
# OVA — Offline VueJS App
<samp>Opinionated Offline First VueJS 3 App Template </samp>
## Features
- Vue3 generated with vue-cli with different plugins:
- babel
- TypeScript
- PWA
- Router
- Vuex
- eslint
- jest
- i18n
- bulma
- pouchdb-browser
- netlify, config ready
## Project setup
```
yarn install
```
### Compiles and hot-reloads for development
```
yarn serve
```
### Compiles and minifies for production
```
yarn build
```
### Run your unit tests
```
yarn test:unit
```
### Lints and fixes files
```
yarn lint
```
### Customize configuration
See [Configuration Reference](https://cli.vuejs.org/config/).
## Why?
Here is my goto configuration when I want to implement an app idea quickly, so here we go, I created a template to simplify my process.

3
babel.config.js Normal file
View File

@@ -0,0 +1,3 @@
module.exports = {
presets: ['@vue/cli-plugin-babel/preset']
}

6
jest.config.js Normal file
View File

@@ -0,0 +1,6 @@
module.exports = {
preset: '@vue/cli-plugin-unit-jest/presets/typescript-and-babel',
transform: {
'^.+\\.vue$': 'vue-jest'
}
}

8
netlify.toml Normal file
View File

@@ -0,0 +1,8 @@
[build]
publish = "dist"
command = "yarn build --modern"
[[redirects]]
from = "/*"
to = "/"
status = 200

50
package.json Normal file
View File

@@ -0,0 +1,50 @@
{
"name": "ova",
"version": "0.1.0",
"private": true,
"scripts": {
"serve": "vue-cli-service serve",
"build": "vue-cli-service build",
"test:unit": "vue-cli-service test:unit",
"lint": "vue-cli-service lint",
"pwa:asset": "npx vue-pwa-asset-generator -a public/img/logo.svg --no-manifest"
},
"dependencies": {
"bulma": "^0.9.2",
"core-js": "^3.9.0",
"pouchdb-adapter-indexeddb": "^7.2.2",
"pouchdb-browser": "^7.2.2",
"register-service-worker": "^1.7.2",
"retrobus": "^1.6.1",
"vue": "^3.0.2",
"vue-i18n": "^9.0.0",
"vue-router": "^4.0.0-rc.2",
"vuex": "^4.0.0-rc.1"
},
"devDependencies": {
"@types/jest": "^26.0.20",
"@types/pouchdb-browser": "^6.1.3",
"@typescript-eslint/eslint-plugin": "^4.15.2",
"@typescript-eslint/parser": "^4.15.2",
"@vue/cli-plugin-babel": "~4.5.11",
"@vue/cli-plugin-eslint": "~4.5.11",
"@vue/cli-plugin-pwa": "~4.5.11",
"@vue/cli-plugin-router": "~4.5.11",
"@vue/cli-plugin-typescript": "~4.5.11",
"@vue/cli-plugin-unit-jest": "~4.5.11",
"@vue/cli-plugin-vuex": "~4.5.11",
"@vue/cli-service": "~4.5.11",
"@vue/compiler-sfc": "^3.0.6",
"@vue/eslint-config-prettier": "^6.0.0",
"@vue/eslint-config-typescript": "^7.0.0",
"@vue/test-utils": "^2.0.0-beta.8",
"eslint": "^7.20.0",
"eslint-plugin-prettier": "^3.3.1",
"eslint-plugin-vue": "^7.6.0",
"node-sass": "^4.14.1",
"prettier": "^1.19.1",
"sass-loader": "^10.1.1",
"typescript": "~4.1.3",
"vue-jest": "^5.0.0-0"
}
}

BIN
public/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 799 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

View File

@@ -0,0 +1,3 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M8.00251 14.9297L0 1.07422H6.14651L8.00251 4.27503L9.84583 1.07422H16L8.00251 14.9297Z" fill="black"/>
</svg>

After

Width:  |  Height:  |  Size: 215 B

3
public/img/logo.svg Normal file
View File

@@ -0,0 +1,3 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M8.00251 14.9297L0 1.07422H6.14651L8.00251 4.27503L9.84583 1.07422H16L8.00251 14.9297Z" fill="black"/>
</svg>

After

Width:  |  Height:  |  Size: 215 B

17
public/index.html Normal file
View File

@@ -0,0 +1,17 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width,initial-scale=1.0">
<link rel="icon" href="<%= BASE_URL %>favicon.ico">
<title><%= htmlWebpackPlugin.options.title %></title>
</head>
<body>
<noscript>
<strong>We're sorry but <%= htmlWebpackPlugin.options.title %> doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
</noscript>
<div id="app"></div>
<!-- built files will be auto injected -->
</body>
</html>

2
public/robots.txt Normal file
View File

@@ -0,0 +1,2 @@
User-agent: *
Disallow:

21
src/App.vue Normal file
View File

@@ -0,0 +1,21 @@
<template>
<div id="app">
<nav>
<router-link to="/">Home</router-link> |
<router-link to="/about">About</router-link>
</nav>
<router-view />
</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue'
export default defineComponent({
name: 'App'
})
</script>
<style lang="scss">
@import 'styles/app';
</style>

BIN
src/assets/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.7 KiB

3
src/bus/busEvent.ts Normal file
View File

@@ -0,0 +1,3 @@
export enum BusEvent {
NEW_VERSION = 'NEW_VERSION'
}

View File

@@ -0,0 +1,34 @@
<template>
<footer class="footer-translation">
<button class="button" @click="toggleLanguage">
{{ t('toggle-lang') }}
</button>
</footer>
</template>
<script lang="ts">
import { defineComponent } from 'vue'
import { locales } from '@/locales/message'
import { useI18n } from 'vue-i18n'
export default defineComponent({
name: 'FooterTranslation',
setup() {
const { t } = useI18n()
const i18n = useI18n()
return {
t,
toggleLanguage: () =>
(i18n.locale.value =
locales[(locales.indexOf(i18n.locale.value) + 1) % locales.length])
}
}
})
</script>
<style lang="scss" scoped>
.footer-translation {
}
</style>

View File

@@ -0,0 +1,184 @@
<template>
<div class="hello">
<h1 class="title is-1">{{ t('hello') }}</h1>
<h2 class="subtitle is-2">{{ upperMessage }}</h2>
<p>
For a guide and recipes on how to configure / customize this project,<br />
check out the
<a href="https://cli.vuejs.org" target="_blank" rel="noopener"
>vue-cli documentation</a
>.
</p>
<h3 class="title is-3">Installed CLI Plugins</h3>
<ul>
<li>
<a
href="https://github.com/vuejs/vue-cli/tree/dev/packages/%40vue/cli-plugin-babel"
target="_blank"
rel="noopener"
>babel</a
>
</li>
<li>
<a
href="https://github.com/vuejs/vue-cli/tree/dev/packages/%40vue/cli-plugin-pwa"
target="_blank"
rel="noopener"
>pwa</a
>
</li>
<li>
<a
href="https://github.com/vuejs/vue-cli/tree/dev/packages/%40vue/cli-plugin-router"
target="_blank"
rel="noopener"
>router</a
>
</li>
<li>
<a
href="https://github.com/vuejs/vue-cli/tree/dev/packages/%40vue/cli-plugin-vuex"
target="_blank"
rel="noopener"
>vuex</a
>
</li>
<li>
<a
href="https://github.com/vuejs/vue-cli/tree/dev/packages/%40vue/cli-plugin-eslint"
target="_blank"
rel="noopener"
>eslint</a
>
</li>
<li>
<a
href="https://github.com/vuejs/vue-cli/tree/dev/packages/%40vue/cli-plugin-unit-jest"
target="_blank"
rel="noopener"
>unit-jest</a
>
</li>
<li>
<a
href="https://github.com/vuejs/vue-cli/tree/dev/packages/%40vue/cli-plugin-typescript"
target="_blank"
rel="noopener"
>typescript</a
>
</li>
</ul>
<h3 class="title is-3">Installed command</h3>
<ul>
<li>
<code>yarn pwa:asset</code> (or <code>npm run pwa:asset</code>) creates
all the assets for the PWA asset. Just change the logo.svg in the img
folder and let the magic do the rest.
</li>
</ul>
<h3 class="title is-3">Essential Links</h3>
<ul>
<li>
<a href="https://vuejs.org" target="_blank" rel="noopener">Core Docs</a>
</li>
<li>
<a href="https://forum.vuejs.org" target="_blank" rel="noopener"
>Forum</a
>
</li>
<li>
<a href="https://chat.vuejs.org" target="_blank" rel="noopener"
>Community Chat</a
>
</li>
<li>
<a href="https://twitter.com/vuejs" target="_blank" rel="noopener"
>Twitter</a
>
</li>
<li>
<a href="https://news.vuejs.org" target="_blank" rel="noopener">News</a>
</li>
</ul>
<h3 class="title is-3">Ecosystem</h3>
<ul>
<li>
<a href="https://router.vuejs.org" target="_blank" rel="noopener"
>vue-router</a
>
</li>
<li>
<a href="https://vuex.vuejs.org" target="_blank" rel="noopener">vuex</a>
</li>
<li>
<a
href="https://github.com/vuejs/vue-devtools#vue-devtools"
target="_blank"
rel="noopener"
>vue-devtools</a
>
</li>
<li>
<a href="https://vue-loader.vuejs.org" target="_blank" rel="noopener"
>vue-loader</a
>
</li>
<li>
<a
href="https://github.com/vuejs/awesome-vue"
target="_blank"
rel="noopener"
>awesome-vue</a
>
</li>
</ul>
</div>
<FooterTranslation class="footer-translation" />
</template>
<script lang="ts">
import { defineComponent, computed } from 'vue'
import { useI18n } from 'vue-i18n'
import FooterTranslation from '@/components/FooterTranslation.vue'
export default defineComponent({
name: 'HelloWorld',
components: {
FooterTranslation
},
props: {
msg: String
},
setup(props) {
const { t } = useI18n()
const upperMessage = computed(() => {
return (props.msg ?? '').toUpperCase()
})
return {
upperMessage,
t
}
}
})
</script>
<style scoped lang="scss">
h3 {
margin: 40px 0 0;
}
ul {
list-style-type: none;
padding: 0;
}
li {
display: inline-block;
margin: 0 10px;
}
a {
color: #42b983;
}
.footer-translation {
margin-top: 1rem;
}
</style>

View File

@@ -0,0 +1 @@
export enum DataType {}

70
src/data/data.ts Normal file
View File

@@ -0,0 +1,70 @@
import PouchDb from 'pouchdb-browser'
import { Model } from './models/Model'
import indexedDb from 'pouchdb-adapter-indexeddb'
import { DataType } from './DataType.enum'
PouchDb.plugin(indexedDb)
interface GetAllParams {
prefix?: string
includeDocs?: boolean
includeAttachments?: boolean
}
class Data {
private locale = new PouchDb('local-db', {
adapter: 'indexeddb'
})
public async add<DT extends DataType>(model: Model<DT>): Promise<boolean> {
try {
const result = await this.locale.put(model)
return result.ok
} catch {
return false
}
}
public async remove(id: string): Promise<boolean> {
try {
const doc = await this.get(id)
if (!doc) {
return false
}
const { ok } = await this.locale.put({
...doc,
_deleted: true
})
return ok
} catch {
return false
}
}
public async get<DT extends DataType, T extends Model<DT>>(
id: string
): Promise<T | null> {
try {
return ((await this.locale.get(id)) as T) || null
} catch {
return null
}
}
public async getAll<DT extends DataType, T extends Model<DT>>({
prefix,
includeDocs = true,
includeAttachments = false
}: GetAllParams): Promise<T[]> {
const response = await this.locale.allDocs({
include_docs: includeDocs,
attachments: includeAttachments,
startkey: prefix ? prefix : undefined,
endkey: prefix ? `${prefix}\ufff0` : undefined
})
return response.rows.map((row) => row.doc) as T[]
}
}
export const data = new Data()

8
src/data/models/Model.ts Normal file
View File

@@ -0,0 +1,8 @@
import { DataType } from '../DataType.enum'
export interface Model<DT extends DataType> {
_id?: string
_rev?: string
_deleted?: boolean
$type: DT
}

4
src/locales/en.json Normal file
View File

@@ -0,0 +1,4 @@
{
"hello": "Hello World",
"toggle-lang": "Toggle lang"
}

4
src/locales/fr.json Normal file
View File

@@ -0,0 +1,4 @@
{
"hello": "Bonjour le monde",
"toggle-lang": "Changer la langue"
}

9
src/locales/message.ts Normal file
View File

@@ -0,0 +1,9 @@
import en from './en.json'
import fr from './fr.json'
export const messages = {
en,
fr
}
export const locales = Object.keys(messages)

19
src/main.ts Normal file
View File

@@ -0,0 +1,19 @@
import '@/registerServiceWorker'
import App from './App.vue'
import { createApp } from 'vue'
import { createI18n } from 'vue-i18n'
import { messages } from '@/locales/message'
import { router } from '@/router/router'
import { store } from '@/store/store'
const i18n = createI18n({
locale: 'en',
messages
})
createApp(App)
.use(store)
.use(router)
.use(i18n)
.mount('#app')

View File

@@ -0,0 +1,37 @@
/* eslint-disable no-console */
import { register } from 'register-service-worker'
import { emit } from 'retrobus'
import { BusEvent } from '@/bus/busEvent'
if (process.env.NODE_ENV === 'production') {
register(`${process.env.BASE_URL}service-worker.js`, {
ready() {
console.log(
'App is being served from cache by a service worker.\n' +
'For more details, visit https://goo.gl/AFskqB'
)
},
registered() {
console.log('Service worker has been registered.')
},
cached() {
console.log('Content has been cached for offline use.')
},
updatefound() {
console.log('New content is downloading.')
},
updated() {
console.log('New content is available; please refresh.')
emit(BusEvent.NEW_VERSION)
},
offline() {
console.log(
'No internet connection found. App is running in offline mode.'
)
},
error(error) {
console.error('Error during service worker registration:', error)
}
})
}

20
src/router/router.ts Normal file
View File

@@ -0,0 +1,20 @@
import { createRouter, createWebHistory, RouteRecordRaw } from 'vue-router'
import Home from '@/views/Home.vue'
const routes: Array<RouteRecordRaw> = [
{
path: '/',
name: 'Home',
component: Home
},
{
path: '/about',
name: 'About',
component: () => import(/* webpackChunkName: "about" */ '@/views/About.vue')
}
]
export const router = createRouter({
history: createWebHistory(process.env.BASE_URL),
routes
})

6
src/shims-vue.d.ts vendored Normal file
View File

@@ -0,0 +1,6 @@
declare module '*.vue' {
import { defineComponent } from 'vue'
const component: ReturnType<typeof defineComponent>
export default component
}
declare module 'pouchdb-adapter-indexeddb'

8
src/store/store.ts Normal file
View File

@@ -0,0 +1,8 @@
import { createStore } from 'vuex'
export const store = createStore({
state: {},
mutations: {},
actions: {},
modules: {}
})

8
src/styles/app.scss Normal file
View File

@@ -0,0 +1,8 @@
@charset "utf-8";
@import '~bulma/bulma.sass';
html,
body {
text-align: center;
min-height: 100vh;
}

5
src/views/About.vue Normal file
View File

@@ -0,0 +1,5 @@
<template>
<div class="about">
<h1>This is an about page</h1>
</div>
</template>

18
src/views/Home.vue Normal file
View File

@@ -0,0 +1,18 @@
<template>
<div class="home">
<img alt="Vue logo" src="@/assets/logo.png" />
<HelloWorld msg="Welcome to OVA Template!" />
</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue'
import HelloWorld from '@/components/HelloWorld.vue'
export default defineComponent({
name: 'Home',
components: {
HelloWorld
}
})
</script>

View File

@@ -0,0 +1,12 @@
import { shallowMount } from '@vue/test-utils'
import HelloWorld from '@/components/HelloWorld.vue'
describe('HelloWorld.vue', () => {
it('renders props.msg when passed', () => {
const msg = 'new message'
const wrapper = shallowMount(HelloWorld, {
props: { msg }
})
expect(wrapper.text()).toMatch(msg)
})
})

29
tsconfig.json Normal file
View File

@@ -0,0 +1,29 @@
{
"compilerOptions": {
"target": "esnext",
"module": "esnext",
"strict": true,
"jsx": "preserve",
"importHelpers": true,
"moduleResolution": "node",
"skipLibCheck": true,
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"sourceMap": true,
"baseUrl": ".",
"resolveJsonModule": true,
"types": ["webpack-env", "jest"],
"paths": {
"@/*": ["src/*"]
},
"lib": ["esnext", "dom", "dom.iterable", "scripthost"]
},
"include": [
"src/**/*.ts",
"src/**/*.tsx",
"src/**/*.vue",
"tests/**/*.ts",
"tests/**/*.tsx"
],
"exclude": ["node_modules"]
}

79
vue.config.js Normal file
View File

@@ -0,0 +1,79 @@
const mainColor = '#3f4fa6'
module.exports = {
pwa: {
themeColor: mainColor,
msTileColor: mainColor,
workboxOptions: {
skipWaiting: true,
clientsClaim: true
},
name: '',
manifestOptions: {
background_color: mainColor,
theme_color: mainColor,
icons: [
{
src: './img/icons/android-chrome-192x192.png',
sizes: '192x192',
type: 'image/png'
},
{
src: './img/icons/android-chrome-512x512.png',
sizes: '512x512',
type: 'image/png'
},
{
src: './img/icons/apple-touch-icon-60x60.png',
sizes: '60x60',
type: 'image/png'
},
{
src: './img/icons/apple-touch-icon-76x76.png',
sizes: '76x76',
type: 'image/png'
},
{
src: './img/icons/apple-touch-icon-120x120.png',
sizes: '120x120',
type: 'image/png'
},
{
src: './img/icons/apple-touch-icon-152x152.png',
sizes: '152x152',
type: 'image/png'
},
{
src: './img/icons/apple-touch-icon-180x180.png',
sizes: '180x180',
type: 'image/png'
},
{
src: './img/icons/apple-touch-icon.png',
sizes: '180x180',
type: 'image/png'
},
{
src: './img/icons/favicon-16x16.png',
sizes: '16x16',
type: 'image/png'
},
{
src: './img/icons/favicon-32x32.png',
sizes: '32x32',
type: 'image/png'
},
{
src: './img/icons/msapplication-icon-144x144.png',
sizes: '144x144',
type: 'image/png'
},
{
src: './img/icons/mstile-150x150.png',
sizes: '150x150',
type: 'image/png'
}
]
}
}
}

11970
yarn.lock Normal file

File diff suppressed because it is too large Load Diff