Initial commit
This commit is contained in:
13
.editorconfig
Normal file
13
.editorconfig
Normal file
@@ -0,0 +1,13 @@
|
||||
# editorconfig.org
|
||||
root = true
|
||||
|
||||
[*]
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
end_of_line = lf
|
||||
charset = utf-8
|
||||
trim_trailing_whitespace = true
|
||||
insert_final_newline = true
|
||||
|
||||
[*.md]
|
||||
trim_trailing_whitespace = false
|
||||
18
.eslintrc.js
Normal file
18
.eslintrc.js
Normal file
@@ -0,0 +1,18 @@
|
||||
module.exports = {
|
||||
root: true,
|
||||
env: {
|
||||
browser: true,
|
||||
node: true,
|
||||
},
|
||||
parserOptions: {
|
||||
parser: '@babel/eslint-parser',
|
||||
requireConfigFile: false,
|
||||
},
|
||||
extends: ['@nuxtjs', 'plugin:nuxt/recommended', 'prettier'],
|
||||
plugins: [],
|
||||
// add your custom rules here
|
||||
rules: {
|
||||
semi: ['error', 'never'],
|
||||
quotes: ['error', 'single']
|
||||
},
|
||||
}
|
||||
90
.gitignore
vendored
Normal file
90
.gitignore
vendored
Normal file
@@ -0,0 +1,90 @@
|
||||
# Created by .ignore support plugin (hsz.mobi)
|
||||
### Node template
|
||||
# Logs
|
||||
/logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
|
||||
# Runtime data
|
||||
pids
|
||||
*.pid
|
||||
*.seed
|
||||
*.pid.lock
|
||||
|
||||
# Directory for instrumented libs generated by jscoverage/JSCover
|
||||
lib-cov
|
||||
|
||||
# Coverage directory used by tools like istanbul
|
||||
coverage
|
||||
|
||||
# nyc test coverage
|
||||
.nyc_output
|
||||
|
||||
# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
|
||||
.grunt
|
||||
|
||||
# Bower dependency directory (https://bower.io/)
|
||||
bower_components
|
||||
|
||||
# node-waf configuration
|
||||
.lock-wscript
|
||||
|
||||
# Compiled binary addons (https://nodejs.org/api/addons.html)
|
||||
build/Release
|
||||
|
||||
# Dependency directories
|
||||
node_modules/
|
||||
jspm_packages/
|
||||
|
||||
# TypeScript v1 declaration files
|
||||
typings/
|
||||
|
||||
# Optional npm cache directory
|
||||
.npm
|
||||
|
||||
# Optional eslint cache
|
||||
.eslintcache
|
||||
|
||||
# Optional REPL history
|
||||
.node_repl_history
|
||||
|
||||
# Output of 'npm pack'
|
||||
*.tgz
|
||||
|
||||
# Yarn Integrity file
|
||||
.yarn-integrity
|
||||
|
||||
# dotenv environment variables file
|
||||
.env
|
||||
|
||||
# parcel-bundler cache (https://parceljs.org/)
|
||||
.cache
|
||||
|
||||
# next.js build output
|
||||
.next
|
||||
|
||||
# nuxt.js build output
|
||||
.nuxt
|
||||
|
||||
# Nuxt generate
|
||||
dist
|
||||
|
||||
# vuepress build output
|
||||
.vuepress/dist
|
||||
|
||||
# Serverless directories
|
||||
.serverless
|
||||
|
||||
# IDE / Editor
|
||||
.idea
|
||||
|
||||
# Service worker
|
||||
sw.*
|
||||
|
||||
# macOS
|
||||
.DS_Store
|
||||
|
||||
# Vim swap files
|
||||
*.swp
|
||||
96
.prettierignore
Normal file
96
.prettierignore
Normal file
@@ -0,0 +1,96 @@
|
||||
###
|
||||
# Place your Prettier ignore content here
|
||||
|
||||
###
|
||||
# .gitignore content is duplicated here due to https://github.com/prettier/prettier/issues/8506
|
||||
|
||||
# Created by .ignore support plugin (hsz.mobi)
|
||||
### Node template
|
||||
# Logs
|
||||
/logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
|
||||
# Runtime data
|
||||
pids
|
||||
*.pid
|
||||
*.seed
|
||||
*.pid.lock
|
||||
|
||||
# Directory for instrumented libs generated by jscoverage/JSCover
|
||||
lib-cov
|
||||
|
||||
# Coverage directory used by tools like istanbul
|
||||
coverage
|
||||
|
||||
# nyc test coverage
|
||||
.nyc_output
|
||||
|
||||
# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
|
||||
.grunt
|
||||
|
||||
# Bower dependency directory (https://bower.io/)
|
||||
bower_components
|
||||
|
||||
# node-waf configuration
|
||||
.lock-wscript
|
||||
|
||||
# Compiled binary addons (https://nodejs.org/api/addons.html)
|
||||
build/Release
|
||||
|
||||
# Dependency directories
|
||||
node_modules/
|
||||
jspm_packages/
|
||||
|
||||
# TypeScript v1 declaration files
|
||||
typings/
|
||||
|
||||
# Optional npm cache directory
|
||||
.npm
|
||||
|
||||
# Optional eslint cache
|
||||
.eslintcache
|
||||
|
||||
# Optional REPL history
|
||||
.node_repl_history
|
||||
|
||||
# Output of 'npm pack'
|
||||
*.tgz
|
||||
|
||||
# Yarn Integrity file
|
||||
.yarn-integrity
|
||||
|
||||
# dotenv environment variables file
|
||||
.env
|
||||
|
||||
# parcel-bundler cache (https://parceljs.org/)
|
||||
.cache
|
||||
|
||||
# next.js build output
|
||||
.next
|
||||
|
||||
# nuxt.js build output
|
||||
.nuxt
|
||||
|
||||
# Nuxt generate
|
||||
dist
|
||||
|
||||
# vuepress build output
|
||||
.vuepress/dist
|
||||
|
||||
# Serverless directories
|
||||
.serverless
|
||||
|
||||
# IDE / Editor
|
||||
.idea
|
||||
|
||||
# Service worker
|
||||
sw.*
|
||||
|
||||
# macOS
|
||||
.DS_Store
|
||||
|
||||
# Vim swap files
|
||||
*.swp
|
||||
4
.prettierrc
Normal file
4
.prettierrc
Normal file
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"semi": false,
|
||||
"singleQuote": true
|
||||
}
|
||||
68
README.md
Normal file
68
README.md
Normal file
@@ -0,0 +1,68 @@
|
||||
# iu-quiz-app
|
||||
|
||||
## Build Setup
|
||||
|
||||
```bash
|
||||
# install dependencies
|
||||
$ npm install
|
||||
|
||||
# serve with hot reload at localhost:3000
|
||||
$ npm run dev
|
||||
|
||||
# build for production and launch server
|
||||
$ npm run build
|
||||
$ npm run start
|
||||
|
||||
# generate static project
|
||||
$ npm run generate
|
||||
```
|
||||
|
||||
For detailed explanation on how things work, check out the [documentation](https://nuxtjs.org).
|
||||
|
||||
## Special Directories
|
||||
|
||||
You can create the following extra directories, some of which have special behaviors. Only `pages` is required; you can delete them if you don't want to use their functionality.
|
||||
|
||||
### `assets`
|
||||
|
||||
The assets directory contains your uncompiled assets such as Stylus or Sass files, images, or fonts.
|
||||
|
||||
More information about the usage of this directory in [the documentation](https://nuxtjs.org/docs/2.x/directory-structure/assets).
|
||||
|
||||
### `components`
|
||||
|
||||
The components directory contains your Vue.js components. Components make up the different parts of your page and can be reused and imported into your pages, layouts and even other components.
|
||||
|
||||
More information about the usage of this directory in [the documentation](https://nuxtjs.org/docs/2.x/directory-structure/components).
|
||||
|
||||
### `layouts`
|
||||
|
||||
Layouts are a great help when you want to change the look and feel of your Nuxt app, whether you want to include a sidebar or have distinct layouts for mobile and desktop.
|
||||
|
||||
More information about the usage of this directory in [the documentation](https://nuxtjs.org/docs/2.x/directory-structure/layouts).
|
||||
|
||||
### `pages`
|
||||
|
||||
This directory contains your application views and routes. Nuxt will read all the `*.vue` files inside this directory and setup Vue Router automatically.
|
||||
|
||||
More information about the usage of this directory in [the documentation](https://nuxtjs.org/docs/2.x/get-started/routing).
|
||||
|
||||
### `plugins`
|
||||
|
||||
The plugins directory contains JavaScript plugins that you want to run before instantiating the root Vue.js Application. This is the place to add Vue plugins and to inject functions or constants. Every time you need to use `Vue.use()`, you should create a file in `plugins/` and add its path to plugins in `nuxt.config.js`.
|
||||
|
||||
More information about the usage of this directory in [the documentation](https://nuxtjs.org/docs/2.x/directory-structure/plugins).
|
||||
|
||||
### `static`
|
||||
|
||||
This directory contains your static files. Each file inside this directory is mapped to `/`.
|
||||
|
||||
Example: `/static/robots.txt` is mapped as `/robots.txt`.
|
||||
|
||||
More information about the usage of this directory in [the documentation](https://nuxtjs.org/docs/2.x/directory-structure/static).
|
||||
|
||||
### `store`
|
||||
|
||||
This directory contains your Vuex store files. Creating a file in this directory automatically activates Vuex.
|
||||
|
||||
More information about the usage of this directory in [the documentation](https://nuxtjs.org/docs/2.x/directory-structure/store).
|
||||
4
assets/variables.scss
Normal file
4
assets/variables.scss
Normal file
@@ -0,0 +1,4 @@
|
||||
// Ref: https://github.com/nuxt-community/vuetify-module#customvariables
|
||||
//
|
||||
// The variables you want to modify
|
||||
// $font-size-root: 20px;
|
||||
43
components/ToastComponent.vue
Normal file
43
components/ToastComponent.vue
Normal file
@@ -0,0 +1,43 @@
|
||||
<template>
|
||||
<v-snackbar
|
||||
v-model="show"
|
||||
dark
|
||||
:color="color"
|
||||
:timeout="timeout"
|
||||
>
|
||||
{{ content }}
|
||||
<template #action="{ attrs }">
|
||||
<v-btn
|
||||
color="primary"
|
||||
text
|
||||
v-bind="attrs"
|
||||
@click="show = false"
|
||||
>
|
||||
Close
|
||||
</v-btn>
|
||||
</template>
|
||||
</v-snackbar>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
show: false,
|
||||
content: '',
|
||||
color: '',
|
||||
timeout: 5000
|
||||
}
|
||||
},
|
||||
created() {
|
||||
this.$store.subscribe((mutation, state) => {
|
||||
if (mutation.type === 'toast/showMessage') {
|
||||
this.content = state.toast.content
|
||||
this.color = state.toast.color
|
||||
this.timeout = state.toast.timeout
|
||||
this.show = true
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
</script>
|
||||
15
jsconfig.json
Normal file
15
jsconfig.json
Normal file
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"~/*": ["./*"],
|
||||
"@/*": ["./*"],
|
||||
"~~/*": ["./*"],
|
||||
"@@/*": ["./*"]
|
||||
}
|
||||
},
|
||||
"vueCompilerOptions": {
|
||||
"target": 2.7
|
||||
},
|
||||
"exclude": ["node_modules", ".nuxt", "dist"]
|
||||
}
|
||||
28
layouts/default.vue
Normal file
28
layouts/default.vue
Normal file
@@ -0,0 +1,28 @@
|
||||
<template>
|
||||
<v-app dark>
|
||||
<v-app-bar fixed app>
|
||||
<v-toolbar-title v-text="title" />
|
||||
</v-app-bar>
|
||||
<v-main>
|
||||
<v-container fill-height>
|
||||
<Nuxt />
|
||||
</v-container>
|
||||
</v-main>
|
||||
<v-footer :absolute="!fixed" app>
|
||||
<span>© {{ new Date().getFullYear() }}</span>
|
||||
</v-footer>
|
||||
<ToastComponent />
|
||||
</v-app>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'DefaultLayout',
|
||||
data() {
|
||||
return {
|
||||
fixed: false,
|
||||
title: 'IU Quiz App',
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
14
layouts/empty.vue
Normal file
14
layouts/empty.vue
Normal file
@@ -0,0 +1,14 @@
|
||||
<template>
|
||||
<v-app>
|
||||
<v-main>
|
||||
<Nuxt />
|
||||
</v-main>
|
||||
</v-app>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'EmptyLayout',
|
||||
layout: 'empty'
|
||||
}
|
||||
</script>
|
||||
43
layouts/error.vue
Normal file
43
layouts/error.vue
Normal file
@@ -0,0 +1,43 @@
|
||||
<template>
|
||||
<v-app dark>
|
||||
<h1 v-if="error.statusCode === 404">
|
||||
{{ pageNotFound }}
|
||||
</h1>
|
||||
<h1 v-else>
|
||||
{{ otherError }}
|
||||
</h1>
|
||||
<NuxtLink to="/"> Home page </NuxtLink>
|
||||
</v-app>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'EmptyLayout',
|
||||
layout: 'empty',
|
||||
props: {
|
||||
error: {
|
||||
type: Object,
|
||||
default: null,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
pageNotFound: '404 Not Found',
|
||||
otherError: 'An error occurred',
|
||||
}
|
||||
},
|
||||
head() {
|
||||
const title =
|
||||
this.error.statusCode === 404 ? this.pageNotFound : this.otherError
|
||||
return {
|
||||
title,
|
||||
}
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
h1 {
|
||||
font-size: 20px;
|
||||
}
|
||||
</style>
|
||||
58
layouts/unverified.vue
Normal file
58
layouts/unverified.vue
Normal file
@@ -0,0 +1,58 @@
|
||||
<template>
|
||||
<v-app>
|
||||
<v-main>
|
||||
<v-container fill-height>
|
||||
<v-card max-width="500px" class="mx-auto">
|
||||
<v-card-title>
|
||||
Verifiziere deine E-Mail-Adresse!
|
||||
</v-card-title>
|
||||
<v-card-text>
|
||||
<p>Wir haben eine E-Mail an deine E-Mail-Adresse <strong>{{ userEmail }}</strong> geschickt.
|
||||
Folge dem Link in der E-Mail, um dein Konto zu verifizieren.</p>
|
||||
<p>Solltest du keine E-Mail erhalten haben, wirf einen Blick in deinen Spam-Ordner.</p>
|
||||
</v-card-text>
|
||||
<v-card-actions>
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn depressed color="primary" :loading="loading" @click="sendVerificationEmail">E-Mail erneut senden</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-container>
|
||||
</v-main>
|
||||
<ToastComponent />
|
||||
</v-app>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { sendEmailVerification } from 'firebase/auth'
|
||||
|
||||
export default {
|
||||
name: 'UnverifiedLayout',
|
||||
layout: 'unverified',
|
||||
data () {
|
||||
return {
|
||||
loading: false
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
userEmail () {
|
||||
return this.$auth.currentUser.email
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
sendVerificationEmail () {
|
||||
this.loading = true
|
||||
|
||||
sendEmailVerification(this.$auth.currentUser)
|
||||
.then(() => {
|
||||
// Email verification sent!
|
||||
this.loading = false
|
||||
this.$toast({
|
||||
content: 'Erfolg! Folge dem Link in der E-Mail, die wir dir gerade geschickt haben, um deine Registrierung abzuschließen!',
|
||||
color: 'success',
|
||||
timeout: -1
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
11
middleware/auth.js
Normal file
11
middleware/auth.js
Normal file
@@ -0,0 +1,11 @@
|
||||
export default function ({ $auth, store, route, redirect }) {
|
||||
// If Firebase Auth hasn't been initialized yet, redirect to index page
|
||||
if (!store.state.firebaseInitialized && route.name !== 'index') {
|
||||
return redirect({ name: 'index' })
|
||||
}
|
||||
// If the user attempts to access any site other than the login page without being logged in,
|
||||
// redirect to login page
|
||||
else if (store.state.firebaseInitialized && (!$auth.currentUser && route.name !== 'login')) {
|
||||
return redirect({ name: 'login' })
|
||||
}
|
||||
}
|
||||
92
nuxt.config.js
Normal file
92
nuxt.config.js
Normal file
@@ -0,0 +1,92 @@
|
||||
import colors from 'vuetify/es5/util/colors'
|
||||
|
||||
export default {
|
||||
// Disable server-side rendering: https://go.nuxtjs.dev/ssr-mode
|
||||
ssr: false,
|
||||
|
||||
// Target: https://go.nuxtjs.dev/config-target
|
||||
target: 'static',
|
||||
|
||||
// Global page headers: https://go.nuxtjs.dev/config-head
|
||||
head: {
|
||||
titleTemplate: '%s - iu-quiz-app',
|
||||
title: 'iu-quiz-app',
|
||||
meta: [
|
||||
{ charset: 'utf-8' },
|
||||
{ name: 'viewport', content: 'width=device-width, initial-scale=1' },
|
||||
{ hid: 'description', name: 'description', content: '' },
|
||||
{ name: 'format-detection', content: 'telephone=no' },
|
||||
],
|
||||
link: [{ rel: 'icon', type: 'image/x-icon', href: '/favicon.ico' }],
|
||||
},
|
||||
|
||||
// Global CSS: https://go.nuxtjs.dev/config-css
|
||||
css: [],
|
||||
|
||||
// Plugins to run before rendering page: https://go.nuxtjs.dev/config-plugins
|
||||
plugins: [
|
||||
'~/plugins/firebase.js',
|
||||
'~/plugins/toast.js'
|
||||
],
|
||||
|
||||
// Auto import components: https://go.nuxtjs.dev/config-components
|
||||
components: true,
|
||||
|
||||
// Modules for dev and build (recommended): https://go.nuxtjs.dev/config-modules
|
||||
buildModules: [
|
||||
// https://go.nuxtjs.dev/eslint
|
||||
'@nuxtjs/eslint-module',
|
||||
// https://go.nuxtjs.dev/vuetify
|
||||
'@nuxtjs/vuetify',
|
||||
],
|
||||
|
||||
// Modules: https://go.nuxtjs.dev/config-modules
|
||||
modules: [
|
||||
// https://go.nuxtjs.dev/pwa
|
||||
'@nuxtjs/pwa',
|
||||
],
|
||||
|
||||
// PWA module configuration: https://go.nuxtjs.dev/pwa
|
||||
pwa: {
|
||||
manifest: {
|
||||
lang: 'en',
|
||||
},
|
||||
},
|
||||
|
||||
router: {
|
||||
middleware: 'auth'
|
||||
},
|
||||
|
||||
// Vuetify module configuration: https://go.nuxtjs.dev/config-vuetify
|
||||
vuetify: {
|
||||
customVariables: ['~/assets/variables.scss'],
|
||||
theme: {
|
||||
dark: false,
|
||||
themes: {
|
||||
light: {
|
||||
// Vuetify default light theme colors
|
||||
primary: colors.blue.darken2,
|
||||
secondary: colors.grey.darken3,
|
||||
accent: colors.blue.accent1,
|
||||
error: colors.red.accent2,
|
||||
info: colors.blue.base,
|
||||
success: colors.green.base,
|
||||
warning: colors.orange.darken1,
|
||||
},
|
||||
dark: {
|
||||
// Vuetify default dark theme colors
|
||||
primary: colors.blue.base,
|
||||
secondary: colors.grey.darken3,
|
||||
accent: colors.pink.accent2,
|
||||
error: colors.red.accent2,
|
||||
info: colors.blue.base,
|
||||
success: colors.green.base,
|
||||
warning: colors.orange.darken1,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
// Build Configuration: https://go.nuxtjs.dev/config-build
|
||||
build: {},
|
||||
}
|
||||
29820
package-lock.json
generated
Normal file
29820
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
37
package.json
Normal file
37
package.json
Normal file
@@ -0,0 +1,37 @@
|
||||
{
|
||||
"name": "iu-quiz-app",
|
||||
"version": "1.0.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "nuxt",
|
||||
"build": "nuxt build",
|
||||
"start": "nuxt start",
|
||||
"generate": "nuxt generate",
|
||||
"lint:js": "eslint --ext \".js,.vue\" --ignore-path .gitignore .",
|
||||
"lint:prettier": "prettier --check .",
|
||||
"lint": "npm run lint:js && npm run lint:prettier",
|
||||
"lintfix": "prettier --write --list-different . && npm run lint:js -- --fix"
|
||||
},
|
||||
"dependencies": {
|
||||
"@nuxtjs/pwa": "^3.3.5",
|
||||
"core-js": "^3.19.3",
|
||||
"firebase": "^9.12.1",
|
||||
"nuxt": "^2.15.8",
|
||||
"vue": "^2.6.14",
|
||||
"vue-server-renderer": "^2.6.14",
|
||||
"vue-template-compiler": "^2.6.14",
|
||||
"vuetify": "^2.6.1",
|
||||
"webpack": "^4.46.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/eslint-parser": "^7.16.3",
|
||||
"@nuxtjs/eslint-config": "^8.0.0",
|
||||
"@nuxtjs/eslint-module": "^3.0.2",
|
||||
"@nuxtjs/vuetify": "^1.12.3",
|
||||
"eslint": "^8.4.1",
|
||||
"eslint-config-prettier": "^8.3.0",
|
||||
"eslint-plugin-nuxt": "^3.1.0",
|
||||
"eslint-plugin-vue": "^8.2.0",
|
||||
"prettier": "^2.5.1"
|
||||
}
|
||||
}
|
||||
24
pages/dashboard.vue
Normal file
24
pages/dashboard.vue
Normal file
@@ -0,0 +1,24 @@
|
||||
<template>
|
||||
<v-row>
|
||||
<v-col class="text-center">
|
||||
<blockquote class="blockquote">
|
||||
“First, solve the problem. Then, write the code.”
|
||||
<footer>
|
||||
<small>
|
||||
<em>—John Johnson</em>
|
||||
</small>
|
||||
</footer>
|
||||
</blockquote>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'DashboardPage',
|
||||
layout ({ $auth }) {
|
||||
// Ref: https://firebase.google.com/docs/reference/js/v8/firebase.auth.Auth#currentuser
|
||||
return $auth.currentUser.emailVerified ? 'default' : 'unverified'
|
||||
}
|
||||
}
|
||||
</script>
|
||||
29
pages/index.vue
Normal file
29
pages/index.vue
Normal file
@@ -0,0 +1,29 @@
|
||||
<template>
|
||||
<v-overlay color="transparent">
|
||||
<v-progress-circular color="primary" indeterminate />
|
||||
</v-overlay>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { onAuthStateChanged } from 'firebase/auth'
|
||||
|
||||
export default {
|
||||
name: 'IndexPage',
|
||||
layout: 'empty',
|
||||
created () {
|
||||
// Setting an authentication state observer on the Firebase Auth object.
|
||||
// This observer gets called when Auth finished initializing and
|
||||
// whenever the user's sign-in state changes.
|
||||
onAuthStateChanged(this.$auth, (user) => {
|
||||
this.$store.commit('initFirebase')
|
||||
if (user) {
|
||||
// User is signed in; redirect to main page (dashboard)
|
||||
this.$router.push({ name: 'dashboard' })
|
||||
} else {
|
||||
// User is signed out; redirect to login page
|
||||
this.$router.push({ name: 'login' })
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
</script>
|
||||
234
pages/login.vue
Normal file
234
pages/login.vue
Normal file
@@ -0,0 +1,234 @@
|
||||
<template>
|
||||
<v-container>
|
||||
<v-card v-if="existingUser" max-width="500px" elevation="12" class="mx-auto">
|
||||
<v-card-text>
|
||||
<v-form ref="loginForm" v-model="valid">
|
||||
<v-text-field
|
||||
v-model="email"
|
||||
label="Deine IU E-Mail-Adresse"
|
||||
prepend-icon="mdi-account-circle"
|
||||
@keyup.enter="signIn"
|
||||
/>
|
||||
<v-text-field
|
||||
v-model="password"
|
||||
:type="showPassword ? 'text' : 'password'"
|
||||
label="Passwort"
|
||||
prepend-icon="mdi-lock"
|
||||
:append-icon="showPassword ? 'mdi-eye' : 'mdi-eye-off'"
|
||||
@click:append="showPassword = !showPassword"
|
||||
@keyup.enter="signIn"
|
||||
/>
|
||||
</v-form>
|
||||
<v-btn
|
||||
block
|
||||
depressed
|
||||
color="primary"
|
||||
:loading="loading"
|
||||
:disabled="loading"
|
||||
@click="signIn"
|
||||
>
|
||||
Anmelden
|
||||
</v-btn>
|
||||
</v-card-text>
|
||||
<v-card-text>
|
||||
<v-btn text block color="primary" @click="existingUser = false">Noch kein Konto?</v-btn>
|
||||
<v-btn text block color="primary" @click="showPwResetDialog = true">Passwort vergessen?</v-btn>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
<v-card v-else max-width="500px" elevation="12" class="mx-auto">
|
||||
<v-card-text>
|
||||
<v-form ref="signupForm" v-model="valid">
|
||||
<v-text-field
|
||||
v-model="email"
|
||||
:rules="emailRules"
|
||||
label="Deine IU E-Mail-Adresse"
|
||||
prepend-icon="mdi-account-circle"
|
||||
@keyup.enter="signUp"
|
||||
/>
|
||||
<v-text-field
|
||||
v-model="password"
|
||||
:rules="passwordRules"
|
||||
:type="showPassword ? 'text' : 'password'"
|
||||
label="Passwort"
|
||||
prepend-icon="mdi-lock"
|
||||
:append-icon="showPassword ? 'mdi-eye' : 'mdi-eye-off'"
|
||||
@click:append="showPassword = !showPassword"
|
||||
@keyup.enter="signUp"
|
||||
/>
|
||||
<v-text-field
|
||||
v-model="passwordConfirm"
|
||||
:rules="passwordConfirmRules"
|
||||
:type="showPassword ? 'text' : 'password'"
|
||||
label="Passwort bestätigen"
|
||||
prepend-icon="mdi-lock"
|
||||
:append-icon="showPassword ? 'mdi-eye' : 'mdi-eye-off'"
|
||||
@click:append="showPassword = !showPassword"
|
||||
@keyup.enter="signUp"
|
||||
/>
|
||||
<v-checkbox
|
||||
v-model="checkbox"
|
||||
:rules="[v => !!v || '']"
|
||||
required
|
||||
>
|
||||
<template #label>
|
||||
<div>
|
||||
Ich stimme den Nutzungs- und Datenschutzbestimmungen zu.
|
||||
</div>
|
||||
</template>
|
||||
</v-checkbox>
|
||||
</v-form>
|
||||
<v-btn
|
||||
block
|
||||
depressed
|
||||
color="primary"
|
||||
:loading="loading"
|
||||
:disabled="loading"
|
||||
@click="signUp"
|
||||
>
|
||||
Registrieren
|
||||
</v-btn>
|
||||
<v-btn
|
||||
text
|
||||
block
|
||||
color="primary"
|
||||
class="mt-3"
|
||||
@click="existingUser = true"
|
||||
>
|
||||
Bereits registriert?
|
||||
</v-btn>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
<v-dialog v-model="showPwResetDialog" max-width="500px">
|
||||
<v-card>
|
||||
<v-card-title>
|
||||
<span class="headline">Passwort zurücksetzen</span>
|
||||
</v-card-title>
|
||||
<v-card-text>
|
||||
<v-form ref="form" v-model="valid" @submit.prevent="resetPassword">
|
||||
<v-text-field v-model="email" autofocus label="Email" required></v-text-field>
|
||||
</v-form>
|
||||
</v-card-text>
|
||||
<v-card-actions>
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn color="primary" outlined @click="showPwResetDialog = false">Abbrechen</v-btn>
|
||||
<v-btn color="primary" depressed :loading="loading" :disabled="loading" @click="resetPassword">Link senden</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
</v-container>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import {
|
||||
createUserWithEmailAndPassword,
|
||||
sendEmailVerification,
|
||||
signInWithEmailAndPassword,
|
||||
sendPasswordResetEmail
|
||||
} from 'firebase/auth'
|
||||
|
||||
export default {
|
||||
name: 'LoginPage',
|
||||
data () {
|
||||
return {
|
||||
existingUser: true,
|
||||
loading: false,
|
||||
valid: false,
|
||||
showPassword: false,
|
||||
showPwResetDialog: false,
|
||||
defaultErrorReqField: 'Feld darf nicht leer sein',
|
||||
email: '',
|
||||
emailRules: [
|
||||
v => !!v || this.defaultErrorReqField,
|
||||
v => /^\w+([.-]?\w+)*@(iu\.org|iubh-fernstudium\.de)+$/.test(v) || 'Keine gültige IU E-Mail-Adresse'
|
||||
],
|
||||
password: '',
|
||||
passwordRules: [
|
||||
v => !!v || this.defaultErrorReqField,
|
||||
v => /^(?=.*?[a-zA-Z])(?=.*?[0-9]).{8,}$/.test(v) || 'Mindestens 8 Zeichen, 1 Buchstabe und 1 Zahl'
|
||||
],
|
||||
passwordConfirm: '',
|
||||
passwordConfirmRules: [
|
||||
v => !!v || this.defaultErrorReqField,
|
||||
v => v === this.password || 'Passwörter stimmen nicht überein'
|
||||
],
|
||||
checkbox: false
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
signUp () {
|
||||
this.$refs.signupForm.validate()
|
||||
if (this.valid) {
|
||||
this.loading = true
|
||||
|
||||
createUserWithEmailAndPassword(this.$auth, this.email, this.password)
|
||||
.then((userCredential) => {
|
||||
// TODO: database integration
|
||||
// User signed up successfully.
|
||||
// The authentication state observer will redirect the user to the main page (dashboard),
|
||||
// see pages/index.vue
|
||||
sendEmailVerification(userCredential.user)
|
||||
.then(() => {
|
||||
// Email verification sent!
|
||||
// Nothing to do here...
|
||||
})
|
||||
})
|
||||
.catch((error) => {
|
||||
// Sign up failed; display error message
|
||||
const errorCode = error.code
|
||||
const errorMessage = error.message
|
||||
this.$toast({content: `${errorCode}: ${errorMessage}`, color: 'error'})
|
||||
})
|
||||
.then(() => {
|
||||
this.loading = false
|
||||
})
|
||||
}
|
||||
},
|
||||
signIn () {
|
||||
this.loading = true
|
||||
|
||||
signInWithEmailAndPassword(this.$auth, this.email, this.password)
|
||||
.then((userCredential) => {
|
||||
// User signed in successfully.
|
||||
// The authentication state observer will redirect the user to the main page (dashboard),
|
||||
// see pages/index.vue
|
||||
})
|
||||
.catch((error) => {
|
||||
// Sign in failed; display error message
|
||||
const errorCode = error.code
|
||||
const errorMessage = error.message
|
||||
this.$toast({content: `${errorCode}: ${errorMessage}`, color: 'error'})
|
||||
})
|
||||
.then(() => {
|
||||
this.loading = false
|
||||
})
|
||||
},
|
||||
resetPassword () {
|
||||
this.$refs.form.validate()
|
||||
if (this.valid) {
|
||||
this.loading = true
|
||||
|
||||
sendPasswordResetEmail(this.$auth, this.email.trim())
|
||||
.then(() => {
|
||||
// Password reset email sent!
|
||||
this.$toast({
|
||||
content:
|
||||
`Wir haben dir einen Link zur Passwortrücksetzung an deine E-Mail-Adresse geschickt!
|
||||
Solltest du keine E-Mail erhalten haben, überprüfe deinen Spam-Ordner.`,
|
||||
color: 'success',
|
||||
timeout: -1
|
||||
})
|
||||
})
|
||||
.catch((error) => {
|
||||
// Failed to send password reset email; display error message
|
||||
const errorCode = error.code
|
||||
const errorMessage = error.message
|
||||
this.$toast({content: `${errorCode}: ${errorMessage}`, color: 'error'})
|
||||
})
|
||||
.then(() => {
|
||||
this.loading = false
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
24
plugins/firebase.js
Normal file
24
plugins/firebase.js
Normal file
@@ -0,0 +1,24 @@
|
||||
import { initializeApp } from 'firebase/app'
|
||||
import { getAuth } from 'firebase/auth'
|
||||
|
||||
// Firebase configuration
|
||||
const firebaseConfig = {
|
||||
apiKey: 'AIzaSyCA_xE5wcYMYNcVkj-diZHbss5Pqb5ZmTA',
|
||||
authDomain: 'iu-quiz-app.firebaseapp.com',
|
||||
projectId: 'iu-quiz-app',
|
||||
storageBucket: 'iu-quiz-app.appspot.com',
|
||||
messagingSenderId: '632060210614',
|
||||
appId: '1:632060210614:web:e2bd45b412b3bd2caba517'
|
||||
}
|
||||
|
||||
// Initialize Firebase
|
||||
const app = initializeApp(firebaseConfig)
|
||||
|
||||
// Initialize Firebase Authentication and get a reference to the service
|
||||
const auth = getAuth(app)
|
||||
|
||||
export default ({ app }, inject) => {
|
||||
// Inject $auth in Vue, context and store.
|
||||
// Ref: https://nuxtjs.org/docs/directory-structure/plugins/
|
||||
inject('auth', auth)
|
||||
}
|
||||
5
plugins/toast.js
Normal file
5
plugins/toast.js
Normal file
@@ -0,0 +1,5 @@
|
||||
export default ({ store, app }, inject) => {
|
||||
// Inject $toast in Vue, context and store.
|
||||
// Ref: https://nuxtjs.org/docs/directory-structure/plugins/
|
||||
inject('toast', payload => store.commit('toast/showMessage', payload))
|
||||
}
|
||||
BIN
static/favicon.ico
Normal file
BIN
static/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 8.4 KiB |
15
store/index.js
Normal file
15
store/index.js
Normal file
@@ -0,0 +1,15 @@
|
||||
export const state = () => ({
|
||||
firebaseInitialized: false
|
||||
})
|
||||
|
||||
export const getters = {
|
||||
}
|
||||
|
||||
export const mutations = {
|
||||
initFirebase (state) {
|
||||
state.firebaseInitialized = true
|
||||
}
|
||||
}
|
||||
|
||||
export const actions = {
|
||||
}
|
||||
14
store/toast.js
Normal file
14
store/toast.js
Normal file
@@ -0,0 +1,14 @@
|
||||
export const state = () => ({
|
||||
content: '',
|
||||
color: '',
|
||||
timeout: 5000
|
||||
})
|
||||
|
||||
export const mutations = {
|
||||
showMessage (state, {content, color, timeout}) {
|
||||
state.content = content
|
||||
state.color = color
|
||||
if (timeout) state.timeout = timeout
|
||||
else state.timeout = 5000
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user