├── .babelrc ├── .editorconfig ├── .eslintrc.js ├── .gitattributes ├── .gitignore ├── .prettierrc ├── LICENSE ├── README.md ├── api.ts ├── app └── router.scrollBehavior.ts ├── assets └── css │ └── tailwind.css ├── components ├── HeadingBar.vue ├── Logo.vue ├── NavBar.vue └── NotificationsBar.vue ├── content ├── article-content.md └── get-started.md ├── interfaces └── index.ts ├── jest.config.js ├── layouts ├── content.vue ├── default.vue └── landing.vue ├── middleware ├── anonymous.ts ├── authenticated.ts └── has-admin-access.ts ├── nuxt.config.js ├── package.json ├── pages ├── admin │ ├── create.vue │ ├── edit │ │ └── _id.vue │ └── index.vue ├── article-content.vue ├── get-started.vue ├── index.vue ├── login.vue ├── main │ ├── dashboard.vue │ └── profile │ │ ├── edit-password.vue │ │ ├── edit.vue │ │ └── index.vue ├── recover-password.vue ├── register.vue └── reset-password.vue ├── plugins └── vee-validate.ts ├── static ├── favicon.ico ├── icon.png └── sw.js ├── store ├── admin │ ├── actions.ts │ ├── getters.ts │ ├── mutations.ts │ └── state.ts ├── helpers │ ├── actions.ts │ ├── getters.ts │ ├── mutations.ts │ └── state.ts └── main │ ├── actions.ts │ ├── getters.ts │ ├── mutations.ts │ └── state.ts ├── tailwind.config.js ├── test └── Logo.spec.js ├── tsconfig.json ├── tslint.json ├── utils.ts └── yarn.lock /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "test": { 4 | "presets": [ 5 | [ 6 | "@babel/preset-env", 7 | { 8 | "targets": { 9 | "node": "current" 10 | } 11 | } 12 | ] 13 | ] 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | env: { 4 | browser: true, 5 | node: true, 6 | }, 7 | extends: [ 8 | "@nuxtjs/eslint-config-typescript", 9 | "plugin:prettier/recommended", 10 | "plugin:nuxt/recommended", 11 | ], 12 | plugins: [], 13 | // add your custom rules here 14 | // https://allurcode.com/custom-linting-rules-in-nuxtjs-and-eslint/ 15 | // https://stackoverflow.com/questions/53516594/why-do-i-keep-getting-delete-cr-prettier-prettier 16 | rules: { 17 | "no-console": process.env.VUE_APP_ENV === "production" ? "error" : "off", 18 | "no-debugger": process.env.VUE_APP_ENV === "production" ? "error" : "off", 19 | "prettier/prettier": [ 20 | "error", 21 | { 22 | endOfLine: "auto", 23 | }, 24 | ], 25 | }, 26 | } 27 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | *.pid.lock 13 | 14 | # Directory for instrumented libs generated by jscoverage/JSCover 15 | lib-cov 16 | 17 | # Coverage directory used by tools like istanbul 18 | coverage 19 | 20 | # nyc test coverage 21 | .nyc_output 22 | 23 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 24 | .grunt 25 | 26 | # Bower dependency directory (https://bower.io/) 27 | bower_components 28 | 29 | # node-waf configuration 30 | .lock-wscript 31 | 32 | # Compiled binary addons (https://nodejs.org/api/addons.html) 33 | build/Release 34 | 35 | # Dependency directories 36 | node_modules/ 37 | jspm_packages/ 38 | 39 | # TypeScript v1 declaration files 40 | typings/ 41 | 42 | # Optional npm cache directory 43 | .npm 44 | 45 | # Optional eslint cache 46 | .eslintcache 47 | 48 | # Optional REPL history 49 | .node_repl_history 50 | 51 | # Output of 'npm pack' 52 | *.tgz 53 | 54 | # Yarn Integrity file 55 | .yarn-integrity 56 | 57 | # dotenv environment variables file 58 | .env 59 | 60 | # parcel-bundler cache (https://parceljs.org/) 61 | .cache 62 | 63 | # next.js build output 64 | .next 65 | 66 | # nuxt.js build output 67 | .nuxt 68 | 69 | # vuepress build output 70 | .vuepress/dist 71 | 72 | # Serverless directories 73 | .serverless 74 | 75 | # FuseBox cache 76 | .fusebox/ 77 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "semi": false, 3 | "singleQuote": false, 4 | "tabWidth": 2, 5 | "endOfLine": "auto" 6 | } 7 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Gavin Chait 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Nuxt.js frontend replacement for FastAPI base project generator 2 | 3 | ## What is it? 4 | 5 | Accelerate your next FastAPI Base Project Generator frontend development by replacing Vue.js with NuxtJS, an open source framework making web development simple and powerful. 6 | 7 | ## Build Setup 8 | 9 | First deploy FastAPI's [Base Project Generator](https://github.com/tiangolo/full-stack-fastapi-postgresql), then replace the entire `frontend` folder with this one, updating the `.env` settings, and `nuxt.config.js`, and `package.json` 'frontend' project name with your own. 10 | 11 | ```bash 12 | # install dependencies 13 | $ yarn install 14 | 15 | # serve with hot reload at localhost:3000 16 | $ yarn dev 17 | 18 | # build for production and launch server 19 | $ yarn build 20 | $ yarn start 21 | 22 | # generate static project 23 | $ yarn generate 24 | ``` 25 | 26 | Hot reload does not work in WSL2 (only WSL1, as of 1 April 2021). For detailed explanation on how things work, check out [Nuxt.js docs](https://nuxtjs.org). 27 | 28 | ## Nuxt.js and components 29 | 30 | - [Nuxt.js](https://nuxtjs.org/) 31 | - [Nuxt-property-decorator](https://github.com/nuxt-community/nuxt-property-decorator) 32 | - [Vue Class Component](https://class-component.vuejs.org/) 33 | 34 | ## TailwindCSS 35 | 36 | - [Tailwindcss](https://tailwindcss.com/) 37 | - [Tailwind heroicons](https://heroicons.com/) 38 | - [Tailwind typography](https://github.com/tailwindlabs/tailwindcss-typography) 39 | 40 | ## Helpers 41 | 42 | - [Nuxt/content](https://content.nuxtjs.org/) 43 | - [Vee-validate](https://vee-validate.logaretm.com/v3/) 44 | - [Nuxt/PWA](https://pwa.nuxtjs.org/) 45 | 46 | Nuxt/PWA is a zero config PWA solution: 47 | 48 | - Registers a service worker for offline caching. 49 | - Automatically generate manifest.json file. 50 | - Automatically adds SEO friendly meta data with manifest integration. 51 | - Automatically generates app icons with different sizes. 52 | - Free background push notifications using OneSignal. 53 | 54 | ## Licence 55 | 56 | This project is licensed under the terms of the MIT license. 57 | -------------------------------------------------------------------------------- /api.ts: -------------------------------------------------------------------------------- 1 | import axios from "axios" 2 | import { 3 | IUserProfile, 4 | IUserProfileUpdate, 5 | IUserProfileCreate, 6 | IUserOpenProfileCreate, 7 | } from "./interfaces" 8 | 9 | function authHeaders(token: string) { 10 | return { 11 | headers: { 12 | Authorization: `Bearer ${token}`, 13 | }, 14 | } 15 | } 16 | 17 | export const api = { 18 | async logInGetToken(username: string, password: string) { 19 | const params = new URLSearchParams() 20 | params.append("username", username) 21 | params.append("password", password) 22 | return await axios.post( 23 | `${process.env.apiUrl}/api/v1/login/access-token`, 24 | params 25 | ) 26 | }, 27 | async createMe(data: IUserOpenProfileCreate) { 28 | return await axios.post(`${process.env.apiUrl}/api/v1/users/open`, data) 29 | }, 30 | async getMe(token: string) { 31 | return await axios.get( 32 | `${process.env.apiUrl}/api/v1/users/me`, 33 | authHeaders(token) 34 | ) 35 | }, 36 | async updateMe(token: string, data: IUserProfileUpdate) { 37 | return await axios.put( 38 | `${process.env.apiUrl}/api/v1/users/me`, 39 | data, 40 | authHeaders(token) 41 | ) 42 | }, 43 | async getUsers(token: string) { 44 | return await axios.get( 45 | `${process.env.apiUrl}/api/v1/users/`, 46 | authHeaders(token) 47 | ) 48 | }, 49 | async updateUser(token: string, userId: number, data: IUserProfileUpdate) { 50 | return await axios.put( 51 | `${process.env.apiUrl}/api/v1/users/${userId}`, 52 | data, 53 | authHeaders(token) 54 | ) 55 | }, 56 | async createUser(token: string, data: IUserProfileCreate) { 57 | return await axios.post( 58 | `${process.env.apiUrl}/api/v1/users/`, 59 | data, 60 | authHeaders(token) 61 | ) 62 | }, 63 | async passwordRecovery(email: string) { 64 | return await axios.post( 65 | `${process.env.apiUrl}/api/v1/password-recovery/${email}` 66 | ) 67 | }, 68 | async resetPassword(password: string, token: string) { 69 | return await axios.post(`${process.env.apiUrl}/api/v1/reset-password/`, { 70 | new_password: password, 71 | token, 72 | }) 73 | }, 74 | } 75 | -------------------------------------------------------------------------------- /app/router.scrollBehavior.ts: -------------------------------------------------------------------------------- 1 | export default function (to: any, from: any, savedPosition: any) { 2 | // https://router.vuejs.org/guide/advanced/scroll-behavior.html 3 | return { x: 0, y: 0 } 4 | } 5 | -------------------------------------------------------------------------------- /assets/css/tailwind.css: -------------------------------------------------------------------------------- 1 | /* ./assets/css/tailwind.css */ 2 | @tailwind base; 3 | @tailwind components; 4 | @tailwind utilities; -------------------------------------------------------------------------------- /components/HeadingBar.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 22 | -------------------------------------------------------------------------------- /components/Logo.vue: -------------------------------------------------------------------------------- 1 | 23 | 24 | 36 | -------------------------------------------------------------------------------- /components/NavBar.vue: -------------------------------------------------------------------------------- 1 | 280 | 281 | 302 | -------------------------------------------------------------------------------- /components/NotificationsBar.vue: -------------------------------------------------------------------------------- 1 | 68 | 69 | 146 | -------------------------------------------------------------------------------- /content/article-content.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Article content 3 | description: 'Empower your NuxtJS application with @nuxt/content module: write in a content/ directory and fetch your Markdown, JSON, YAML and CSV files through a MongoDB like API, acting as a Git-based Headless CMS.' 4 | --- 5 | 6 | Empower your NuxtJS application with `@nuxtjs/content` module: write in a `content/` directory and fetch your Markdown, JSON, YAML and CSV files through a MongoDB like API, acting as a **Git-based Headless CMS**. 7 | 8 | ## Writing content 9 | 10 | Learn how to write your `content/`, supporting Markdown, YAML, CSV and JSON: https://content.nuxtjs.org/writing. 11 | 12 | ## Fetching content 13 | 14 | Learn how to fetch your content with `$content`: https://content.nuxtjs.org/fetching. 15 | 16 | ## Displaying content 17 | 18 | Learn how to display your Markdown content with the `` component directly in your template: https://content.nuxtjs.org/displaying. -------------------------------------------------------------------------------- /content/get-started.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Getting started 3 | description: 'Accelerate your next FastAPI Base Project Generator frontend development by replacing Vue.js with NuxtJS, an open source framework making web development simple and powerful.' 4 | --- 5 | 6 | Accelerate your next FastAPI Base Project Generator frontend development by replacing Vue.js with NuxtJS, an open source framework making web development simple and powerful. 7 | 8 | ## Build Setup 9 | 10 | First deploy FastAPI's [Base Project Generator](https://github.com/tiangolo/full-stack-fastapi-postgresql), then replace the entire `frontend` folder with this one, updating the `.env` settings, and `nuxt.config.js`, and `package.json` 'frontend' project name with your own. 11 | 12 | ```bash 13 | # install dependencies 14 | $ yarn install 15 | 16 | # serve with hot reload at localhost:3000 17 | $ yarn dev 18 | 19 | # build for production and launch server 20 | $ yarn build 21 | $ yarn start 22 | 23 | # generate static project 24 | $ yarn generate 25 | ``` 26 | 27 | Hot reload does not work in WSL2 (only WSL1, as of 1 April 2021). For detailed explanation on how things work, check out [Nuxt.js docs](https://nuxtjs.org). 28 | 29 | ## Nuxt.js and components 30 | 31 | - [Nuxt.js](https://nuxtjs.org/) 32 | - [Nuxt-property-decorator](https://github.com/nuxt-community/nuxt-property-decorator) 33 | - [Vue Class Component](https://class-component.vuejs.org/) 34 | 35 | ## TailwindCSS 36 | 37 | - [Tailwindcss](https://tailwindcss.com/) 38 | - [Tailwind heroicons](https://heroicons.com/) 39 | - [Tailwind typography](https://github.com/tailwindlabs/tailwindcss-typography) 40 | 41 | ## Helpers 42 | 43 | - [Nuxt/content](https://content.nuxtjs.org/) 44 | - [Vee-validate](https://vee-validate.logaretm.com/v3/) 45 | - [Nuxt/PWA](https://pwa.nuxtjs.org/) 46 | 47 | Nuxt/PWA is a zero config PWA solution: 48 | 49 | - Registers a service worker for offline caching. 50 | - Automatically generate manifest.json file. 51 | - Automatically adds SEO friendly meta data with manifest integration. 52 | - Automatically generates app icons with different sizes. 53 | - Free background push notifications using OneSignal. 54 | 55 | ## Licence 56 | 57 | This project is licensed under the terms of the MIT license. -------------------------------------------------------------------------------- /interfaces/index.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable camelcase */ 2 | export interface IUserProfile { 3 | email: string 4 | is_active: boolean 5 | is_superuser: boolean 6 | full_name: string 7 | id: number 8 | } 9 | 10 | export interface IUserProfileUpdate { 11 | email?: string 12 | full_name?: string 13 | password?: string 14 | is_active?: boolean 15 | is_superuser?: boolean 16 | } 17 | 18 | export interface IUserProfileCreate { 19 | email: string 20 | full_name?: string 21 | password?: string 22 | is_active?: boolean 23 | is_superuser?: boolean 24 | } 25 | 26 | export interface IUserOpenProfileCreate { 27 | email: string 28 | full_name?: string 29 | password: string 30 | } 31 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | moduleNameMapper: { 3 | '^@/(.*)$': '/$1', 4 | '^~/(.*)$': '/$1', 5 | '^vue$': 'vue/dist/vue.common.js', 6 | }, 7 | moduleFileExtensions: ['ts', 'js', 'vue', 'json'], 8 | transform: { 9 | '^.+\\.ts$': 'ts-jest', 10 | '^.+\\.js$': 'babel-jest', 11 | '.*\\.(vue)$': 'vue-jest', 12 | }, 13 | collectCoverage: true, 14 | collectCoverageFrom: [ 15 | '/components/**/*.vue', 16 | '/pages/**/*.vue', 17 | ], 18 | } 19 | -------------------------------------------------------------------------------- /layouts/content.vue: -------------------------------------------------------------------------------- 1 | 15 | -------------------------------------------------------------------------------- /layouts/default.vue: -------------------------------------------------------------------------------- 1 | 13 | -------------------------------------------------------------------------------- /layouts/landing.vue: -------------------------------------------------------------------------------- 1 | 192 | 193 | 206 | -------------------------------------------------------------------------------- /middleware/anonymous.ts: -------------------------------------------------------------------------------- 1 | export default function ({ store, redirect }) { 2 | if (store.getters["main/isLoggedIn"]) { 3 | return redirect("/") 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /middleware/authenticated.ts: -------------------------------------------------------------------------------- 1 | export default function ({ store, redirect }) { 2 | if (!store.getters["main/isLoggedIn"]) { 3 | return redirect("/login") 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /middleware/has-admin-access.ts: -------------------------------------------------------------------------------- 1 | export default function ({ store, redirect }) { 2 | if ( 3 | !store.getters["main/hasAdminAccess"] && 4 | !store.getters["main/isLoggedIn"] 5 | ) { 6 | return redirect("/") 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /nuxt.config.js: -------------------------------------------------------------------------------- 1 | export default { 2 | // Global page headers: https://go.nuxtjs.dev/config-head 3 | head: { 4 | title: "frontend", 5 | meta: [ 6 | { charset: "utf-8" }, 7 | { name: "viewport", content: "width=device-width, initial-scale=1" }, 8 | { hid: "description", name: "description", content: "" }, 9 | ], 10 | link: [{ rel: "icon", type: "image/x-icon", href: "/favicon.ico" }], 11 | }, 12 | 13 | // Env: https://nuxtjs.org/docs/2.x/configuration-glossary/configuration-env/ 14 | env: { 15 | baseUrl: process.env.BASE_URL || "http://localhost:3000", 16 | appName: process.env.VUE_APP_NAME, 17 | apiUrl: `http://${process.env.VUE_APP_DOMAIN}`, 18 | }, 19 | 20 | // Global CSS: https://go.nuxtjs.dev/config-css 21 | css: [], 22 | 23 | // Plugins to run before rendering page: https://go.nuxtjs.dev/config-plugins 24 | plugins: ["~/plugins/vee-validate"], 25 | 26 | // Auto import components: https://go.nuxtjs.dev/config-components 27 | components: true, 28 | 29 | // Modules for dev and build (recommended): https://go.nuxtjs.dev/config-modules 30 | buildModules: [ 31 | // https://go.nuxtjs.dev/typescript 32 | "@nuxt/typescript-build", 33 | // https://go.nuxtjs.dev/tailwindcss 34 | "@nuxtjs/tailwindcss", 35 | ], 36 | 37 | // Modules: https://go.nuxtjs.dev/config-modules 38 | modules: [ 39 | // https://go.nuxtjs.dev/axios 40 | "@nuxtjs/axios", 41 | // https://go.nuxtjs.dev/pwa 42 | "@nuxtjs/pwa", 43 | // https://go.nuxtjs.dev/content 44 | "@nuxt/content", 45 | // https://i18n.nuxtjs.org/ 46 | "nuxt-i18n", 47 | ], 48 | 49 | // Axios module configuration: https://go.nuxtjs.dev/config-axios 50 | axios: {}, 51 | 52 | // nuxt/i18n module configuration: https://i18n.nuxtjs.org/basic-usage 53 | i18n: { 54 | locales: ["en", "fr", "es"], 55 | defaultLocale: "en", 56 | vueI18n: { 57 | fallbackLocale: "en", 58 | messages: { 59 | en: { 60 | welcome: "Welcome", 61 | }, 62 | fr: { 63 | welcome: "Bienvenue", 64 | }, 65 | es: { 66 | welcome: "Bienvenido", 67 | }, 68 | }, 69 | }, 70 | detectBrowserLanguage: { 71 | useCookie: true, 72 | cookieKey: "i18n_redirected", 73 | onlyOnRoot: true, // recommended 74 | }, 75 | }, 76 | 77 | // PWA module configuration: https://go.nuxtjs.dev/pwa 78 | pwa: { 79 | manifest: { 80 | lang: "en", 81 | }, 82 | }, 83 | 84 | // Content module configuration: https://go.nuxtjs.dev/config-content 85 | content: {}, 86 | 87 | // Build Configuration: https://go.nuxtjs.dev/config-build 88 | build: { 89 | transpile: ["vee-validate/dist/rules"], 90 | }, 91 | } 92 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "frontend", 3 | "version": "1.0.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "nuxt", 7 | "build": "nuxt build", 8 | "start": "nuxt start", 9 | "generate": "nuxt generate", 10 | "lint:js": "eslint --ext \".js,.vue\" --ignore-path .gitignore .", 11 | "lint": "yarn lint:js", 12 | "test": "jest" 13 | }, 14 | "dependencies": { 15 | "@nuxt/content": "^1.14.0", 16 | "@nuxtjs/axios": "^5.13.1", 17 | "@nuxtjs/pwa": "^3.3.5", 18 | "@tailwindcss/forms": "^0.3.2", 19 | "@tailwindcss/typography": "^0.4.0", 20 | "core-js": "^3.9.1", 21 | "nuxt": "^2.15.3", 22 | "nuxt-i18n": "^6.24.0", 23 | "nuxt-property-decorator": "^2.9.1", 24 | "vee-validate": "^3.4.5" 25 | }, 26 | "devDependencies": { 27 | "@nuxt/types": "^2.15.3", 28 | "@nuxt/typescript-build": "^2.1.0", 29 | "@nuxtjs/eslint-config-typescript": "^6.0.0", 30 | "@nuxtjs/eslint-module": "^3.0.2", 31 | "@nuxtjs/tailwindcss": "^4.0.3", 32 | "@tailwindcss/postcss7-compat": "^2.1.0", 33 | "@vue/test-utils": "^1.1.3", 34 | "autoprefixer": "^9", 35 | "babel-core": "7.0.0-bridge.0", 36 | "babel-eslint": "^10.1.0", 37 | "babel-jest": "^26.6.3", 38 | "eslint": "^7.22.0", 39 | "eslint-config-prettier": "^8.1.0", 40 | "eslint-plugin-nuxt": "^2.0.0", 41 | "eslint-plugin-prettier": "^3.3.1", 42 | "eslint-plugin-vue": "^7.7.0", 43 | "jest": "^26.6.3", 44 | "postcss": "^7", 45 | "prettier": "^2.2.1", 46 | "tailwindcss": "npm:@tailwindcss/postcss7-compat", 47 | "ts-jest": "^26.5.4", 48 | "vue-jest": "^3.0.4" 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /pages/admin/create.vue: -------------------------------------------------------------------------------- 1 | 154 | 155 | 207 | 208 | 222 | -------------------------------------------------------------------------------- /pages/admin/edit/_id.vue: -------------------------------------------------------------------------------- 1 | 169 | 170 | 245 | 246 | 260 | -------------------------------------------------------------------------------- /pages/admin/index.vue: -------------------------------------------------------------------------------- 1 | 114 | 115 | 134 | -------------------------------------------------------------------------------- /pages/article-content.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 23 | -------------------------------------------------------------------------------- /pages/get-started.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 23 | -------------------------------------------------------------------------------- /pages/index.vue: -------------------------------------------------------------------------------- 1 | 204 | 205 | 213 | -------------------------------------------------------------------------------- /pages/login.vue: -------------------------------------------------------------------------------- 1 | 100 | 101 | 125 | -------------------------------------------------------------------------------- /pages/main/dashboard.vue: -------------------------------------------------------------------------------- 1 | 19 | 20 | 44 | 45 | 53 | -------------------------------------------------------------------------------- /pages/main/profile/edit-password.vue: -------------------------------------------------------------------------------- 1 | 77 | 78 | 106 | 107 | 118 | -------------------------------------------------------------------------------- /pages/main/profile/edit.vue: -------------------------------------------------------------------------------- 1 | 67 | 68 | 113 | 114 | 125 | -------------------------------------------------------------------------------- /pages/main/profile/index.vue: -------------------------------------------------------------------------------- 1 | 66 | 67 | 81 | 82 | 87 | -------------------------------------------------------------------------------- /pages/recover-password.vue: -------------------------------------------------------------------------------- 1 | 76 | 77 | 98 | -------------------------------------------------------------------------------- /pages/register.vue: -------------------------------------------------------------------------------- 1 | 278 | 279 | 314 | -------------------------------------------------------------------------------- /pages/reset-password.vue: -------------------------------------------------------------------------------- 1 | 175 | 176 | 208 | 209 | 220 | -------------------------------------------------------------------------------- /plugins/vee-validate.ts: -------------------------------------------------------------------------------- 1 | import Vue from "vue" 2 | import { ValidationObserver, ValidationProvider, extend } from "vee-validate" 3 | import { required, confirmed, email } from "vee-validate/dist/rules" 4 | 5 | extend("required", { 6 | ...required, 7 | message: "true", 8 | }) 9 | 10 | extend("confirmed", { 11 | ...confirmed, 12 | message: "true", 13 | }) 14 | 15 | extend("email", { 16 | ...email, 17 | message: "Please use a valid email address.", 18 | }) 19 | 20 | // Register it globally 21 | Vue.component("ValidationProvider", ValidationProvider) 22 | Vue.component("ValidationObserver", ValidationObserver) 23 | -------------------------------------------------------------------------------- /static/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/whythawk/nuxt-for-fastapi/bdd7a72d09253eeae40b255cf6025dc264dbc87e/static/favicon.ico -------------------------------------------------------------------------------- /static/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/whythawk/nuxt-for-fastapi/bdd7a72d09253eeae40b255cf6025dc264dbc87e/static/icon.png -------------------------------------------------------------------------------- /static/sw.js: -------------------------------------------------------------------------------- 1 | // THIS FILE SHOULD NOT BE VERSION CONTROLLED 2 | 3 | // https://github.com/NekR/self-destroying-sw 4 | 5 | self.addEventListener('install', function (e) { 6 | self.skipWaiting() 7 | }) 8 | 9 | self.addEventListener('activate', function (e) { 10 | self.registration.unregister() 11 | .then(function () { 12 | return self.clients.matchAll() 13 | }) 14 | .then(function (clients) { 15 | clients.forEach(client => client.navigate(client.url)) 16 | }) 17 | }) 18 | -------------------------------------------------------------------------------- /store/admin/actions.ts: -------------------------------------------------------------------------------- 1 | import { api } from "@/api" 2 | import { IUserProfileCreate, IUserProfileUpdate } from "@/interfaces" 3 | // Review global namespacing https://vuex.vuejs.org/guide/modules.html 4 | 5 | export default { 6 | async getUsers({ commit, dispatch, rootState }) { 7 | try { 8 | const response = await api.getUsers(rootState.main.token) 9 | if (response) { 10 | await commit("setUsers", response.data) 11 | } 12 | } catch (error) { 13 | await dispatch("main/checkApiError", error, { root: true }) 14 | } 15 | }, 16 | async updateUser( 17 | { commit, dispatch, rootState }, 18 | payload: { id: number; user: IUserProfileUpdate } 19 | ) { 20 | try { 21 | const loadingNotification = { 22 | content: "Saving...", 23 | showProgress: true, 24 | } 25 | await commit("main/addNotification", loadingNotification, { root: true }) 26 | const response = ( 27 | await Promise.all([ 28 | api.updateUser(rootState.main.token, payload.id, payload.user), 29 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 30 | await new Promise((resolve) => 31 | setTimeout(() => resolve(), 500) 32 | ), 33 | ]) 34 | )[0] 35 | await commit("setUser", response.data) 36 | await commit("main/removeNotification", loadingNotification, { 37 | root: true, 38 | }) 39 | await commit( 40 | "main/addNotification", 41 | { 42 | content: "User successfully updated", 43 | color: "success", 44 | }, 45 | { root: true } 46 | ) 47 | } catch (error) { 48 | await dispatch("main/checkApiError", error, { root: true }) 49 | } 50 | }, 51 | async createUser( 52 | { commit, dispatch, rootState }, 53 | payload: IUserProfileCreate 54 | ) { 55 | try { 56 | const loadingNotification = { content: "Saving...", showProgress: true } 57 | await commit("main/addNotification", loadingNotification, { root: true }) 58 | const response = ( 59 | await Promise.all([ 60 | api.createUser(rootState.main.token, payload), 61 | await new Promise((resolve) => 62 | setTimeout(() => resolve(), 500) 63 | ), 64 | ]) 65 | )[0] 66 | await commit("setUser", response.data) 67 | await commit("main/removeNotification", loadingNotification, { 68 | root: true, 69 | }) 70 | await commit( 71 | "main/addNotification", 72 | { 73 | content: "User successfully created", 74 | color: "success", 75 | }, 76 | { root: true } 77 | ) 78 | } catch (error) { 79 | await dispatch("main/checkApiError", error, { root: true }) 80 | } 81 | }, 82 | } 83 | -------------------------------------------------------------------------------- /store/admin/getters.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | adminUsers: (state) => state.users, 3 | adminOneUser: (state) => (userId: number) => { 4 | const filteredUsers = state.users.filter((user) => user.id === userId) 5 | if (filteredUsers.length > 0) { 6 | return { ...filteredUsers[0] } 7 | } 8 | }, 9 | } 10 | -------------------------------------------------------------------------------- /store/admin/mutations.ts: -------------------------------------------------------------------------------- 1 | import { IUserProfile } from "@/interfaces" 2 | 3 | export default { 4 | setUsers(state, payload: IUserProfile[]) { 5 | state.users = payload 6 | }, 7 | setUser(state, payload: IUserProfile) { 8 | const users = state.users.filter( 9 | (user: IUserProfile) => user.id !== payload.id 10 | ) 11 | users.push(payload) 12 | state.users = users 13 | }, 14 | } 15 | -------------------------------------------------------------------------------- /store/admin/state.ts: -------------------------------------------------------------------------------- 1 | import { IUserProfile } from "@/interfaces" 2 | 3 | export interface AdminState { 4 | users: IUserProfile[] 5 | } 6 | 7 | const defaultState: AdminState = { 8 | users: [], 9 | } 10 | 11 | export default () => defaultState 12 | -------------------------------------------------------------------------------- /store/helpers/actions.ts: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/whythawk/nuxt-for-fastapi/bdd7a72d09253eeae40b255cf6025dc264dbc87e/store/helpers/actions.ts -------------------------------------------------------------------------------- /store/helpers/getters.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | headingTitle: (state) => state.headingTitle, 3 | } 4 | -------------------------------------------------------------------------------- /store/helpers/mutations.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | setHeadingTitle(state, payload: string) { 3 | state.headingTitle = payload 4 | }, 5 | } 6 | -------------------------------------------------------------------------------- /store/helpers/state.ts: -------------------------------------------------------------------------------- 1 | export interface HelperState { 2 | headingTitle: string | null 3 | } 4 | 5 | const defaultState: HelperState = { 6 | headingTitle: null, 7 | } 8 | 9 | export default () => defaultState 10 | -------------------------------------------------------------------------------- /store/main/actions.ts: -------------------------------------------------------------------------------- 1 | import { api } from "@/api" 2 | import { getLocalToken, removeLocalToken, saveLocalToken } from "@/utils" 3 | import { AxiosError } from "axios" 4 | import { IUserOpenProfileCreate } from "@/interfaces" 5 | import { AppNotification } from "./state" 6 | 7 | export default { 8 | async logIn( 9 | { commit, dispatch }, 10 | payload: { username: string; password: string } 11 | ) { 12 | try { 13 | const response = await api.logInGetToken( 14 | payload.username, 15 | payload.password 16 | ) 17 | const token = response.data.access_token 18 | if (token) { 19 | saveLocalToken(token) 20 | await commit("setToken", token) 21 | await commit("setLoggedIn", true) 22 | await commit("setLogInError", false) 23 | await dispatch("getUserProfile") 24 | } else { 25 | await dispatch("logOut") 26 | } 27 | } catch (error) { 28 | commit("setLogInError", true) 29 | await dispatch("logOut") 30 | } 31 | }, 32 | async getUserProfile({ commit, dispatch, state }) { 33 | try { 34 | const response = await api.getMe(state.token) 35 | if (response.data) { 36 | await commit("setUserProfile", response.data) 37 | } 38 | } catch (error) { 39 | await dispatch("checkApiError", error) 40 | } 41 | }, 42 | async updateUserProfile({ commit, dispatch, state }, payload) { 43 | try { 44 | const loadingNotification = { content: "Saving...", showProgress: true } 45 | await commit("addNotification", loadingNotification) 46 | const response = ( 47 | await Promise.all([ 48 | api.updateMe(state.token, payload), 49 | await new Promise((resolve) => 50 | setTimeout(() => resolve(), 500) 51 | ), 52 | ]) 53 | )[0] 54 | await commit("setUserProfile", response.data) 55 | await commit("removeNotification", loadingNotification) 56 | await commit("addNotification", { 57 | content: "Profile successfully updated", 58 | color: "success", 59 | }) 60 | } catch (error) { 61 | await dispatch("checkApiError", error) 62 | } 63 | }, 64 | async createUserProfile( 65 | { commit, dispatch }, 66 | payload: IUserOpenProfileCreate 67 | ) { 68 | try { 69 | const loadingNotification = { content: "Creating...", showProgress: true } 70 | await commit("addNotification", loadingNotification) 71 | await Promise.all([ 72 | api.createMe(payload), 73 | await new Promise((resolve) => setTimeout(() => resolve(), 500)), 74 | ]) 75 | await commit("removeNotification", loadingNotification) 76 | await commit("addNotification", { 77 | content: "Account successfully created", 78 | color: "success", 79 | }) 80 | } catch (error) { 81 | // console.log(error.response) 82 | await dispatch("checkApiError", error) 83 | } 84 | }, 85 | async checkLoggedIn({ commit, dispatch, state }) { 86 | if (!state.isLoggedIn) { 87 | let token = state.token 88 | if (!token) { 89 | const localToken = getLocalToken() 90 | if (localToken) { 91 | await commit("setToken", token) 92 | token = localToken 93 | } 94 | } 95 | if (token) { 96 | try { 97 | const response = await api.getMe(token) 98 | await commit("setLoggedIn", true) 99 | await commit("setUserProfile", response.data) 100 | } catch (error) { 101 | await dispatch("logOut") 102 | } 103 | } else { 104 | await dispatch("logOut") 105 | } 106 | } 107 | }, 108 | async logOut({ commit }) { 109 | removeLocalToken() 110 | await commit("setToken", "") 111 | await commit("setLoggedIn", false) 112 | await commit("setUserProfile", null) 113 | }, 114 | async checkApiError({ dispatch }, payload: AxiosError) { 115 | // console.log(payload.response) 116 | if (payload.response!.status === 401) { 117 | await dispatch("logOut") 118 | } 119 | }, 120 | async removeNotification( 121 | { commit }, 122 | payload: { notification: AppNotification; timeout: number } 123 | ) { 124 | return await new Promise((resolve) => { 125 | setTimeout(() => { 126 | commit("removeNotification", payload.notification) 127 | resolve(true) 128 | }, payload.timeout) 129 | }) 130 | }, 131 | async passwordRecovery({ commit, dispatch }, payload: { username: string }) { 132 | try { 133 | await Promise.all([ 134 | api.passwordRecovery(payload.username), 135 | await new Promise((resolve) => setTimeout(() => resolve(), 500)), 136 | ]) 137 | } catch (error) {} 138 | // Refactored this ... shouldn't give user indication if their attempt was successful or not 139 | await dispatch("logOut") 140 | const loadingNotification = { 141 | content: "Sending password recovery email", 142 | showProgress: true, 143 | } 144 | await commit("addNotification", loadingNotification) 145 | }, 146 | async resetPassword( 147 | { commit, dispatch }, 148 | payload: { password: string; token: string } 149 | ) { 150 | const loadingNotification = { 151 | content: "Resetting password", 152 | showProgress: true, 153 | } 154 | try { 155 | await commit("addNotification", loadingNotification) 156 | await Promise.all([ 157 | api.resetPassword(payload.password, payload.token), 158 | await new Promise((resolve) => setTimeout(() => resolve(), 500)), 159 | ]) 160 | await commit("removeNotification", loadingNotification) 161 | await commit("addNotification", { 162 | content: "Password successfully reset", 163 | color: "success", 164 | }) 165 | await dispatch("logOut") 166 | } catch (error) { 167 | await commit("removeNotification", loadingNotification) 168 | await commit("addNotification", { 169 | color: "error", 170 | content: "Error resetting password", 171 | }) 172 | } 173 | }, 174 | } 175 | -------------------------------------------------------------------------------- /store/main/getters.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | hasAdminAccess: (state) => { 3 | return ( 4 | state.userProfile && 5 | state.userProfile.is_superuser && 6 | state.userProfile.is_active 7 | ) 8 | }, 9 | loginError: (state) => state.logInError, 10 | dashboardShowDrawer: (state) => state.dashboardShowDrawer, 11 | dashboardMiniDrawer: (state) => state.dashboardMiniDrawer, 12 | userProfile: (state) => state.userProfile, 13 | token: (state) => state.token, 14 | isLoggedIn: (state) => state.isLoggedIn, 15 | firstNotification: (state) => 16 | state.notifications.length > 0 && state.notifications[0], 17 | } 18 | -------------------------------------------------------------------------------- /store/main/mutations.ts: -------------------------------------------------------------------------------- 1 | import { IUserProfile } from "@/interfaces" 2 | import { AppNotification } from "./state" 3 | 4 | export default { 5 | setToken(state, payload: string) { 6 | state.token = payload 7 | }, 8 | setLoggedIn(state, payload: boolean) { 9 | state.isLoggedIn = payload 10 | }, 11 | setLogInError(state, payload: boolean) { 12 | state.logInError = payload 13 | }, 14 | setUserProfile(state, payload: IUserProfile) { 15 | state.userProfile = payload 16 | }, 17 | setDashboardMiniDrawer(state, payload: boolean) { 18 | state.dashboardMiniDrawer = payload 19 | }, 20 | setDashboardShowDrawer(state, payload: boolean) { 21 | state.dashboardShowDrawer = payload 22 | }, 23 | addNotification(state, payload: AppNotification) { 24 | state.notifications.push(payload) 25 | }, 26 | removeNotification(state, payload: AppNotification) { 27 | state.notifications = state.notifications.filter( 28 | (notification) => notification !== payload 29 | ) 30 | }, 31 | } 32 | -------------------------------------------------------------------------------- /store/main/state.ts: -------------------------------------------------------------------------------- 1 | import { IUserProfile } from "@/interfaces" 2 | 3 | export interface AppNotification { 4 | content: string 5 | color?: string 6 | showProgress?: boolean 7 | } 8 | 9 | export interface MainState { 10 | token: string 11 | isLoggedIn: boolean | null 12 | logInError: boolean 13 | userProfile: IUserProfile | null 14 | notifications: AppNotification[] 15 | } 16 | 17 | const defaultState: MainState = { 18 | isLoggedIn: null, 19 | token: "", 20 | logInError: false, 21 | userProfile: null, 22 | notifications: [], 23 | } 24 | 25 | export default () => defaultState 26 | -------------------------------------------------------------------------------- /tailwind.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | purge: [ 3 | "./components/**/*.{vue,js,ts}", 4 | "./layouts/**/*.vue", 5 | "./pages/**/*.vue", 6 | "./plugins/**/*.{js,ts}", 7 | "./nuxt.config.{js,ts}", 8 | ], 9 | darkMode: false, // or 'media' or 'class' 10 | // https://devdojo.com/tnylea/custom-animations-in-tailwindcss 11 | theme: { 12 | extend: { 13 | keyframes: { 14 | "fade-in": { 15 | "0%": { 16 | opacity: "0", 17 | }, 18 | "100%": { 19 | opacity: "1", 20 | }, 21 | }, 22 | "fade-out": { 23 | from: { 24 | opacity: "1", 25 | }, 26 | to: { 27 | opacity: "0", 28 | }, 29 | }, 30 | }, 31 | animation: { 32 | "fade-in": "fade-in 0.5s ease-out", 33 | "fade-out": "fade-out 0.5s ease-out", 34 | }, 35 | }, 36 | }, 37 | variants: { 38 | extend: {}, 39 | }, 40 | plugins: [require("@tailwindcss/typography"), require("@tailwindcss/forms")], 41 | } 42 | -------------------------------------------------------------------------------- /test/Logo.spec.js: -------------------------------------------------------------------------------- 1 | import { mount } from '@vue/test-utils' 2 | import Logo from '@/components/Logo.vue' 3 | 4 | describe('Logo', () => { 5 | test('is a Vue instance', () => { 6 | const wrapper = mount(Logo) 7 | expect(wrapper.vm).toBeTruthy() 8 | }) 9 | }) 10 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "noImplicitAny": false, 4 | "target": "ES2018", 5 | "module": "ESNext", 6 | "moduleResolution": "Node", 7 | "lib": [ 8 | "ESNext", 9 | "ESNext.AsyncIterable", 10 | "DOM" 11 | ], 12 | "esModuleInterop": true, 13 | "allowJs": true, 14 | "sourceMap": true, 15 | "strict": true, 16 | "noEmit": true, 17 | "experimentalDecorators": true, 18 | "baseUrl": ".", 19 | "paths": { 20 | "~/*": [ 21 | "./*" 22 | ], 23 | "@/*": [ 24 | "./*" 25 | ] 26 | }, 27 | "types": [ 28 | "@nuxt/types", 29 | "@nuxtjs/axios", 30 | "@nuxt/content", 31 | "@types/node" 32 | ] 33 | }, 34 | "exclude": [ 35 | "node_modules", 36 | ".nuxt", 37 | "dist" 38 | ] 39 | } 40 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "defaultSeverity": "warning", 3 | "extends": [ 4 | "tslint:recommended" 5 | ], 6 | "linterOptions": { 7 | "exclude": [ 8 | "node_modules/**" 9 | ] 10 | }, 11 | "rules": { 12 | "quotemark": [true, "single"], 13 | "indent": [true, "spaces", 2], 14 | "interface-name": false, 15 | "ordered-imports": false, 16 | "object-literal-sort-keys": false, 17 | "no-consecutive-blank-lines": false 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /utils.ts: -------------------------------------------------------------------------------- 1 | export const getLocalToken = () => localStorage.getItem("token") 2 | 3 | export const saveLocalToken = (token: string) => 4 | localStorage.setItem("token", token) 5 | 6 | export const removeLocalToken = () => localStorage.removeItem("token") 7 | --------------------------------------------------------------------------------