merge master

This commit is contained in:
Mohd Arief
2018-05-15 12:35:52 +08:00
321 changed files with 15736 additions and 797 deletions
+15 -19
View File
@@ -1,22 +1,18 @@
import Vue from 'vue'
import store from '~/store'
import router from '~/router'
import i18n from '~/plugins/i18n'
import App from '~/components/App'
/**
* First we will load all of this project's JavaScript dependencies which
* includes Vue and other libraries. It is a great starting point when
* building robust, powerful web applications using Vue and Laravel.
*/
import '~/plugins'
import '~/components'
require('./bootstrap');
Vue.config.productionTip = false
window.Vue = require('vue');
/**
* Next, we will create a fresh Vue application instance and attach it to
* the page. Then, you may begin adding components to this application
* or customize the JavaScript scaffolding to fit your unique needs.
*/
Vue.component('example-component', require('./components/ExampleComponent.vue'));
const app = new Vue({
el: '#app'
});
/* eslint-disable no-new */
new Vue({
i18n,
store,
router,
...App
})
-56
View File
@@ -1,56 +0,0 @@
window._ = require('lodash');
window.Popper = require('popper.js').default;
/**
* We'll load jQuery and the Bootstrap jQuery plugin which provides support
* for JavaScript based Bootstrap features such as modals and tabs. This
* code may be modified to fit the specific needs of your application.
*/
try {
window.$ = window.jQuery = require('jquery');
require('bootstrap');
} catch (e) {}
/**
* We'll load the axios HTTP library which allows us to easily issue requests
* to our Laravel back-end. This library automatically handles sending the
* CSRF token as a header based on the value of the "XSRF" token cookie.
*/
window.axios = require('axios');
window.axios.defaults.headers.common['X-Requested-With'] = 'XMLHttpRequest';
/**
* Next we will register the CSRF Token as a common header with Axios so that
* all outgoing HTTP requests automatically have it attached. This is just
* a simple convenience so we don't have to attach every token manually.
*/
let token = document.head.querySelector('meta[name="csrf-token"]');
if (token) {
window.axios.defaults.headers.common['X-CSRF-TOKEN'] = token.content;
} else {
console.error('CSRF token not found: https://laravel.com/docs/csrf#csrf-x-csrf-token');
}
/**
* Echo exposes an expressive API for subscribing to channels and listening
* for events that are broadcast by Laravel. Echo and event broadcasting
* allows your team to easily build robust real-time web applications.
*/
// import Echo from 'laravel-echo'
// window.Pusher = require('pusher-js');
// window.Echo = new Echo({
// broadcaster: 'pusher',
// key: process.env.MIX_PUSHER_APP_KEY,
// cluster: process.env.MIX_PUSHER_APP_CLUSTER,
// encrypted: true
// });
+66
View File
@@ -0,0 +1,66 @@
<template>
<div id="app">
<loading ref="loading"/>
<transition name="page" mode="out-in">
<component v-if="layout" :is="layout"/>
</transition>
</div>
</template>
<script>
import Loading from './Loading'
// Load layout components dynamically.
const requireContext = require.context('~/layouts', false, /.*\.vue$/)
const layouts = requireContext.keys()
.map(file =>
[file.replace(/(^.\/)|(\.vue$)/g, ''), requireContext(file)]
)
.reduce((components, [name, component]) => {
components[name] = component.default || component
return components
}, {})
export default {
el: '#app',
components: {
Loading
},
data: () => ({
layout: null,
defaultLayout: 'default'
}),
metaInfo () {
const { appName } = window.config
return {
title: appName,
titleTemplate: `%s · ${appName}`
}
},
mounted () {
this.$loading = this.$refs.loading
},
methods: {
/**
* Set the application layout.
*
* @param {String} layout
*/
setLayout (layout) {
if (!layout || !layouts[layout]) {
layout = this.defaultLayout
}
this.layout = layouts[layout]
}
}
}
</script>
+43
View File
@@ -0,0 +1,43 @@
<template>
<button :type="nativeType" :disabled="loading" :class="{
[`btn-${type}`]: true,
'btn-block': block,
'btn-lg': large,
'btn-loading': loading
}" class="btn">
<slot/>
</button>
</template>
<script>
export default {
name: 'VButton',
props: {
type: {
type: String,
default: 'primary'
},
nativeType: {
type: String,
default: 'submit'
},
loading: {
type: Boolean,
default: false
},
block: {
type: Boolean,
default: false
},
large: {
type: Boolean,
default: false
}
}
}
</script>
+21
View File
@@ -0,0 +1,21 @@
<template>
<div class="card">
<div v-if="title" class="card-header">
{{ title }}
</div>
<div class="card-body">
<slot/>
</div>
</div>
</template>
<script>
export default {
name: 'Card',
props: {
title: { type: String, default: null }
}
}
</script>
@@ -0,0 +1,65 @@
<template>
<div class="custom-control custom-checkbox d-flex">
<input
:name="name"
:checked="internalValue"
:id="id || name"
type="checkbox"
class="custom-control-input"
@click="handleClick">
<label :for="id || name" class="custom-control-label my-auto">
<slot/>
</label>
</div>
</template>
<script>
export default {
name: 'Checkbox',
props: {
id: { type: String, default: null },
name: { type: String, default: 'checkbox' },
value: { type: Boolean, default: false },
checked: { type: Boolean, default: false }
},
data: () => ({
internalValue: false
}),
watch: {
value (val) {
this.internalValue = val
},
checked (val) {
this.internalValue = val
},
internalValue (val, oldVal) {
if (val !== oldVal) {
this.$emit('input', val)
}
}
},
created () {
this.internalValue = this.value
if ('checked' in this.$options.propsData) {
this.internalValue = this.checked
}
},
methods: {
handleClick (e) {
this.$emit('click', e)
if (!e.isPropagationStopped) {
this.internalValue = e.target.checked
}
}
}
}
</script>
+13
View File
@@ -0,0 +1,13 @@
<template>
<transition name="page" mode="out-in">
<slot>
<router-view/>
</slot>
</transition>
</template>
<script>
export default {
name: 'Child'
}
</script>
@@ -1,23 +0,0 @@
<template>
<div class="container">
<div class="row justify-content-center">
<div class="col-md-8">
<div class="card card-default">
<div class="card-header">Example Component</div>
<div class="card-body">
I'm an example component.
</div>
</div>
</div>
</div>
</div>
</template>
<script>
export default {
mounted() {
console.log('Component mounted.')
}
}
</script>
+100
View File
@@ -0,0 +1,100 @@
<template>
<div :style="{
width: `${percent}%`,
height: height,
opacity: show ? 1 : 0,
'background-color': canSuccess ? color : failedColor
}" class="progress"/>
</template>
<script>
// https://github.com/nuxt/nuxt.js/blob/master/lib/app/components/nuxt-loading.vue
export default {
data: () => ({
percent: 0,
show: false,
canSuccess: true,
duration: 3000,
height: '2px',
color: '#77b6ff',
failedColor: 'red'
}),
methods: {
start () {
this.show = true
this.canSuccess = true
if (this._timer) {
clearInterval(this._timer)
this.percent = 0
}
this._cut = 10000 / Math.floor(this.duration)
this._timer = setInterval(() => {
this.increase(this._cut * Math.random())
if (this.percent > 95) {
this.finish()
}
}, 100)
return this
},
set (num) {
this.show = true
this.canSuccess = true
this.percent = Math.floor(num)
return this
},
get () {
return Math.floor(this.percent)
},
increase (num) {
this.percent = this.percent + Math.floor(num)
return this
},
decrease (num) {
this.percent = this.percent - Math.floor(num)
return this
},
finish () {
this.percent = 100
this.hide()
return this
},
pause () {
clearInterval(this._timer)
return this
},
hide () {
clearInterval(this._timer)
this._timer = null
setTimeout(() => {
this.show = false
this.$nextTick(() => {
setTimeout(() => {
this.percent = 0
}, 200)
})
}, 500)
return this
},
fail () {
this.canSuccess = false
return this
}
}
}
</script>
<style scoped>
.progress {
position: fixed;
top: 0px;
left: 0px;
right: 0px;
height: 2px;
width: 0%;
transition: width 0.2s, opacity 0.4s;
opacity: 1;
background-color: #efc14e;
z-index: 999999;
}
</style>
@@ -0,0 +1,36 @@
<template>
<li class="nav-item dropdown">
<a class="nav-link dropdown-toggle" href="#" role="button"
data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
{{ locales[locale] }}
</a>
<div class="dropdown-menu">
<a v-for="(value, key) in locales" :key="key" class="dropdown-item" href="#"
@click.prevent="setLocale(key)">
{{ value }}
</a>
</div>
</li>
</template>
<script>
import { mapGetters } from 'vuex'
import { loadMessages } from '~/plugins/i18n'
export default {
computed: mapGetters({
locale: 'lang/locale',
locales: 'lang/locales'
}),
methods: {
setLocale (locale) {
if (this.$i18n.locale !== locale) {
loadMessages(locale)
this.$store.dispatch('lang/setLocale', { locale })
}
}
}
}
</script>
@@ -0,0 +1,84 @@
<template>
<button v-if="githubAuth" class="btn btn-dark ml-auto" type="button" @click="login">
{{ $t('login_with') }}
<fa :icon="['fab', 'github']"/>
</button>
</template>
<script>
export default {
name: 'LoginWithGithub',
computed: {
githubAuth: () => window.config.githubAuth,
url: () => `/api/oauth/github`
},
mounted () {
window.addEventListener('message', this.onMessage, false)
},
beforeDestroy () {
window.removeEventListener('message', this.onMessage)
},
methods: {
async login () {
const url = await this.$store.dispatch('auth/fetchOauthUrl', {
provider: 'github'
})
openWindow(url, this.$t('login'))
},
/**
* @param {MessageEvent} e
*/
onMessage (e) {
if (e.origin !== window.origin || !e.data.token) {
return
}
this.$store.dispatch('auth/saveToken', {
token: e.data.token
})
this.$router.push({ name: 'home' })
}
}
}
/**
* @param {Object} options
* @return {Window}
*/
function openWindow (url, title, options = {}) {
if (typeof url === 'object') {
options = url
url = ''
}
options = { url, title, width: 600, height: 720, ...options }
const dualScreenLeft = window.screenLeft !== undefined ? window.screenLeft : window.screen.left
const dualScreenTop = window.screenTop !== undefined ? window.screenTop : window.screen.top
const width = window.innerWidth || document.documentElement.clientWidth || window.screen.width
const height = window.innerHeight || document.documentElement.clientHeight || window.screen.height
options.left = ((width / 2) - (options.width / 2)) + dualScreenLeft
options.top = ((height / 2) - (options.height / 2)) + dualScreenTop
const optionsStr = Object.keys(options).reduce((acc, key) => {
acc.push(`${key}=${options[key]}`)
return acc
}, []).join(',')
const newWindow = window.open(url, title, optionsStr)
if (window.focus) {
newWindow.focus()
}
return newWindow
}
</script>
+95
View File
@@ -0,0 +1,95 @@
<template>
<nav class="navbar navbar-expand-lg navbar-light bg-white">
<div class="container">
<router-link :to="{ name: user ? 'home' : 'welcome' }" class="navbar-brand">
{{ appName }}
</router-link>
<button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarToggler" aria-controls="navbarToggler" aria-expanded="false">
<span class="navbar-toggler-icon"/>
</button>
<div id="navbarToggler" class="collapse navbar-collapse">
<ul class="navbar-nav">
<locale-dropdown/>
<!-- <li class="nav-item">
<a class="nav-link" href="#">Link</a>
</li> -->
</ul>
<ul class="navbar-nav ml-auto">
<!-- Authenticated -->
<li v-if="user" class="nav-item dropdown">
<a class="nav-link dropdown-toggle text-dark"
href="#" role="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
<img :src="user.photo_url" class="rounded-circle profile-photo mr-1">
{{ user.name }}
</a>
<div class="dropdown-menu">
<router-link :to="{ name: 'settings.profile' }" class="dropdown-item pl-3">
<fa icon="cog" fixed-width/>
{{ $t('settings') }}
</router-link>
<div class="dropdown-divider"/>
<a href="#" class="dropdown-item pl-3" @click.prevent="logout">
<fa icon="sign-out-alt" fixed-width/>
{{ $t('logout') }}
</a>
</div>
</li>
<!-- Guest -->
<template v-else>
<li class="nav-item">
<router-link :to="{ name: 'login' }" class="nav-link" active-class="active">
{{ $t('login') }}
</router-link>
</li>
<li class="nav-item">
<router-link :to="{ name: 'register' }" class="nav-link" active-class="active">
{{ $t('register') }}
</router-link>
</li>
</template>
</ul>
</div>
</div>
</nav>
</template>
<script>
import { mapGetters } from 'vuex'
import LocaleDropdown from './LocaleDropdown'
export default {
components: {
LocaleDropdown
},
data: () => ({
appName: window.config.appName
}),
computed: mapGetters({
user: 'auth/user'
}),
methods: {
async logout () {
// Log out the user.
await this.$store.dispatch('auth/logout')
// Redirect to login.
this.$router.push({ name: 'login' })
}
}
}
</script>
<style scoped>
.profile-photo {
width: 2rem;
height: 2rem;
margin: -.375rem 0;
}
</style>
+19
View File
@@ -0,0 +1,19 @@
import Vue from 'vue'
import Card from './Card'
import Child from './Child'
import Button from './Button'
import Checkbox from './Checkbox'
import { HasError, AlertError, AlertSuccess } from 'vform'
// Components that are registered globaly.
[
Card,
Child,
Button,
Checkbox,
HasError,
AlertError,
AlertSuccess
].forEach(Component => {
Vue.component(Component.name, Component)
})
+34
View File
@@ -0,0 +1,34 @@
{
"ok": "Ok",
"cancel": "Cancel",
"error_alert_title": "Oops...",
"error_alert_text": "Something went wrong! Please try again.",
"token_expired_alert_title": "Session Expired!",
"token_expired_alert_text": "Please log in again to continue.",
"login": "Log In",
"register": "Register",
"page_not_found": "Page Not Found",
"go_home": "Go Home",
"logout": "Logout",
"email": "Email",
"remember_me": "Remember Me",
"password": "Password",
"forgot_password": "Forgot Your Password?",
"confirm_password": "Confirm Password",
"name": "Name",
"toggle_navigation": "Toggle navigation",
"home": "Home",
"you_are_logged_in": "You are logged in!",
"reset_password": "Reset Password",
"send_password_reset_link": "Send Password Reset Link",
"settings": "Settings",
"profile": "Profile",
"your_info": "Your Info",
"info_updated": "Your info has been updated!",
"update": "Update",
"your_password": "Your Password",
"password_updated": "Your password has been updated!",
"new_password": "New Password",
"login_with": "Login with",
"register_with": "Register with"
}
+34
View File
@@ -0,0 +1,34 @@
{
"ok": "De Acuerdo",
"cancel": "Cancelar",
"error_alert_title": "Ha ocurrido un problema",
"error_alert_text": "¡Algo salió mal! Inténtalo de nuevo.",
"token_expired_alert_title": "!Sesión Expirada!",
"token_expired_alert_text": "Por favor inicie sesión de nuevo para continuar.",
"login": "Iniciar Sesión",
"register": "Registro",
"page_not_found": "Página No Encontrada",
"go_home": "Ir a Inicio",
"logout": "Cerrar Sesión",
"email": "Correo Electrónico",
"remember_me": "Recuérdame",
"password": "Contraseña",
"forgot_password": "¿Olvidaste tu contraseña?",
"confirm_password": "Confirmar Contraseña",
"name": "Nombre",
"toggle_navigation": "Cambiar Navegación",
"home": "Inicio",
"you_are_logged_in": "¡Has iniciado sesión!",
"reset_password": "Restablecer la contraseña",
"send_password_reset_link": "Enviar Enlace de Restablecimiento de Contraseña",
"settings": "Configuraciones",
"profile": "Perfil",
"your_info": "Tu Información",
"info_updated": "¡Tu información ha sido actualizada!",
"update": "Actualizar",
"your_password": "Tu Contraseña",
"password_updated": "¡Tu contraseña ha sido actualizada!",
"new_password": "Nueva Contraseña",
"login_with": "Iniciar Sesión con",
"register_with": "Registro con"
}
+34
View File
@@ -0,0 +1,34 @@
{
"ok": "确定",
"cancel": "取消",
"error_alert_title": "错误...",
"error_alert_text": "遇到一些错误,请稍后重试~",
"token_expired_alert_title": "验证过期!",
"token_expired_alert_text": "请稍后重新登录系统",
"login": "登录",
"register": "注册",
"page_not_found": "页面不存在",
"go_home": "返回首页",
"logout": "退出",
"email": "邮箱",
"remember_me": "记住我",
"password": "密码",
"forgot_password": "忘记密码?",
"confirm_password": "重复密码",
"name": "用户名",
"toggle_navigation": "切换导航",
"home": "首页",
"you_are_logged_in": "您已经登录!",
"reset_password": "重置密码",
"send_password_reset_link": "发送重置链接",
"settings": "设置",
"profile": "个人设置",
"your_info": "您的个人信息",
"info_updated": "您的个人信息已经更改!",
"update": "更新",
"your_password": "您的密码",
"password_updated": "您的密码已经更新!",
"new_password": "新密码",
"login_with": "登录",
"register_with": "注册"
}
+30
View File
@@ -0,0 +1,30 @@
<template>
<div class="basic-layout d-flex align-items-center justify-content-center m-0 bg-white">
<child/>
</div>
</template>
<script>
export default {
name: 'BasicLayout'
}
</script>
<style lang="scss">
.basic-layout {
color: #636b6f;
height: 100vh;
font-weight: 100;
position: relative;
.links > a {
color: #636b6f;
padding: 0 25px;
font-size: 12px;
font-weight: 600;
letter-spacing: .1rem;
text-decoration: none;
text-transform: uppercase;
}
}
</style>
+21
View File
@@ -0,0 +1,21 @@
<template>
<div class="main-layout">
<navbar/>
<div class="container mt-4">
<child/>
</div>
</div>
</template>
<script>
import Navbar from '~/components/Navbar'
export default {
name: 'MainLayout',
components: {
Navbar
}
}
</script>
+9
View File
@@ -0,0 +1,9 @@
import store from '~/store'
export default (to, from, next) => {
if (store.getters['auth/user'].role !== 'admin') {
next({ name: 'home' })
} else {
next()
}
}
+9
View File
@@ -0,0 +1,9 @@
import store from '~/store'
export default async (to, from, next) => {
if (!store.getters['auth/check']) {
next({ name: 'login' })
} else {
next()
}
}
@@ -0,0 +1,11 @@
import store from '~/store'
export default async (to, from, next) => {
if (!store.getters['auth/check'] && store.getters['auth/token']) {
try {
await store.dispatch('auth/fetchUser')
} catch (e) { }
}
next()
}
+9
View File
@@ -0,0 +1,9 @@
import store from '~/store'
export default (to, from, next) => {
if (store.getters['auth/check']) {
next({ name: 'home' })
} else {
next()
}
}
+8
View File
@@ -0,0 +1,8 @@
import store from '~/store'
import { loadMessages } from '~/plugins/i18n'
export default async (to, from, next) => {
await loadMessages(store.getters['lang/locale'])
next()
}
+15
View File
@@ -0,0 +1,15 @@
<template>
<card :title="$t('home')">
This is admin
</card>
</template>
<script>
export default {
middleware: 'admin',
metaInfo () {
return { title: this.$t('home') }
}
}
</script>
+58
View File
@@ -0,0 +1,58 @@
<template>
<div>
<div class="top-right links">
<template v-if="authenticated">
<router-link :to="{ name: 'home' }">
{{ $t('home') }}
</router-link>
</template>
<template v-else>
<router-link :to="{ name: 'login' }">
{{ $t('login') }}
</router-link>
<router-link :to="{ name: 'register' }">
{{ $t('register') }}
</router-link>
</template>
</div>
<div class="text-center">
<div class="title mb-4">
This is admin
</div>
</div>
</div>
</template>
<script>
import { mapGetters } from 'vuex'
export default {
layout: 'basic',
metaInfo () {
return { title: this.$t('home') }
},
data: () => ({
title: window.config.appName
}),
computed: mapGetters({
authenticated: 'auth/check'
})
}
</script>
<style scoped>
.top-right {
position: absolute;
right: 10px;
top: 18px;
}
.title {
font-size: 85px;
}
</style>
+100
View File
@@ -0,0 +1,100 @@
<template>
<div class="row">
<div class="col-lg-8 m-auto">
<card :title="$t('login')">
<form @submit.prevent="login" @keydown="form.onKeydown($event)">
<!-- Email -->
<div class="form-group row">
<label class="col-md-3 col-form-label text-md-right">{{ $t('email') }}</label>
<div class="col-md-7">
<input v-model="form.email" :class="{ 'is-invalid': form.errors.has('email') }" class="form-control" type="email" name="email">
<has-error :form="form" field="email"/>
</div>
</div>
<!-- Password -->
<div class="form-group row">
<label class="col-md-3 col-form-label text-md-right">{{ $t('password') }}</label>
<div class="col-md-7">
<input v-model="form.password" :class="{ 'is-invalid': form.errors.has('password') }" class="form-control" type="password" name="password">
<has-error :form="form" field="password"/>
</div>
</div>
<!-- Remember Me -->
<div class="form-group row">
<div class="col-md-3"/>
<div class="col-md-7 d-flex">
<checkbox v-model="remember" name="remember">
{{ $t('remember_me') }}
</checkbox>
<router-link :to="{ name: 'password.request' }" class="small ml-auto my-auto">
{{ $t('forgot_password') }}
</router-link>
</div>
</div>
<div class="form-group row">
<div class="col-md-7 offset-md-3 d-flex">
<!-- Submit Button -->
<v-button :loading="form.busy">
{{ $t('login') }}
</v-button>
<!-- GitHub Login Button -->
<login-with-github/>
</div>
</div>
</form>
</card>
</div>
</div>
</template>
<script>
import Form from 'vform'
import LoginWithGithub from '~/components/LoginWithGithub'
export default {
middleware: 'guest',
components: {
LoginWithGithub
},
metaInfo () {
return { title: this.$t('login') }
},
data: () => ({
form: new Form({
email: '',
password: ''
}),
remember: false
}),
methods: {
async login () {
// Submit the form.
const { data } = await this.form.post('/api/login')
// Save the token.
this.$store.dispatch('auth/saveToken', {
token: data.token,
remember: this.remember
})
// Fetch the user.
await this.$store.dispatch('auth/fetchUser')
// Redirect home.
if(store.getters['auth/user'].role === 'admin'){
this.$router.push({ name: 'admin.home' })
}
this.$router.push({ name: 'home' })
}
}
}
</script>
@@ -0,0 +1,58 @@
<template>
<div class="row">
<div class="col-lg-8 m-auto">
<card :title="$t('reset_password')">
<form @submit.prevent="send" @keydown="form.onKeydown($event)">
<alert-success :form="form" :message="status"/>
<!-- Email -->
<div class="form-group row">
<label class="col-md-3 col-form-label text-md-right">{{ $t('email') }}</label>
<div class="col-md-7">
<input v-model="form.email" :class="{ 'is-invalid': form.errors.has('email') }" class="form-control" type="email" name="email">
<has-error :form="form" field="email"/>
</div>
</div>
<!-- Submit Button -->
<div class="form-group row">
<div class="col-md-9 ml-md-auto">
<v-button :loading="form.busy">
{{ $t('send_password_reset_link') }}
</v-button>
</div>
</div>
</form>
</card>
</div>
</div>
</template>
<script>
import Form from 'vform'
export default {
middleware: 'guest',
metaInfo () {
return { title: this.$t('reset_password') }
},
data: () => ({
status: '',
form: new Form({
email: ''
})
}),
methods: {
async send () {
const { data } = await this.form.post('/api/password/email')
this.status = data.status
this.form.reset()
}
}
}
</script>
@@ -0,0 +1,84 @@
<template>
<div class="row">
<div class="col-lg-8 m-auto">
<card :title="$t('reset_password')">
<form @submit.prevent="reset" @keydown="form.onKeydown($event)">
<alert-success :form="form" :message="status"/>
<!-- Email -->
<div class="form-group row">
<label class="col-md-3 col-form-label text-md-right">{{ $t('email') }}</label>
<div class="col-md-7">
<input v-model="form.email" :class="{ 'is-invalid': form.errors.has('email') }" class="form-control" type="email" name="email" readonly>
<has-error :form="form" field="email"/>
</div>
</div>
<!-- Password -->
<div class="form-group row">
<label class="col-md-3 col-form-label text-md-right">{{ $t('password') }}</label>
<div class="col-md-7">
<input v-model="form.password" :class="{ 'is-invalid': form.errors.has('password') }" class="form-control" type="password" name="password">
<has-error :form="form" field="password"/>
</div>
</div>
<!-- Password Confirmation -->
<div class="form-group row">
<label class="col-md-3 col-form-label text-md-right">{{ $t('confirm_password') }}</label>
<div class="col-md-7">
<input v-model="form.password_confirmation" :class="{ 'is-invalid': form.errors.has('password_confirmation') }" class="form-control" type="password" name="password_confirmation">
<has-error :form="form" field="password_confirmation"/>
</div>
</div>
<!-- Submit Button -->
<div class="form-group row">
<div class="col-md-9 ml-md-auto">
<v-button :loading="form.busy">
{{ $t('reset_password') }}
</v-button>
</div>
</div>
</form>
</card>
</div>
</div>
</template>
<script>
import Form from 'vform'
export default {
middleware: 'guest',
metaInfo () {
return { title: this.$t('reset_password') }
},
data: () => ({
status: '',
form: new Form({
token: '',
email: '',
password: '',
password_confirmation: ''
})
}),
created () {
this.form.email = this.$route.query.email
this.form.token = this.$route.params.token
},
methods: {
async reset () {
const { data } = await this.form.post('/api/password/reset')
this.status = data.status
this.form.reset()
}
}
}
</script>
+105
View File
@@ -0,0 +1,105 @@
<template>
<div class="row">
<div class="col-lg-8 m-auto">
<card :title="$t('register')">
<form @submit.prevent="register" @keydown="form.onKeydown($event)">
<!-- Name -->
<div class="form-group row">
<label class="col-md-3 col-form-label text-md-right">{{ $t('name') }}</label>
<div class="col-md-7">
<input v-model="form.name" :class="{ 'is-invalid': form.errors.has('name') }" class="form-control" type="text" name="name">
<has-error :form="form" field="name"/>
</div>
</div>
<!-- Email -->
<div class="form-group row">
<label class="col-md-3 col-form-label text-md-right">{{ $t('email') }}</label>
<div class="col-md-7">
<input v-model="form.email" :class="{ 'is-invalid': form.errors.has('email') }" class="form-control" type="email" name="email">
<has-error :form="form" field="email"/>
</div>
</div>
<!-- Password -->
<div class="form-group row">
<label class="col-md-3 col-form-label text-md-right">{{ $t('password') }}</label>
<div class="col-md-7">
<input v-model="form.password" :class="{ 'is-invalid': form.errors.has('password') }" class="form-control" type="password" name="password">
<has-error :form="form" field="password"/>
</div>
</div>
<!-- Password Confirmation -->
<div class="form-group row">
<label class="col-md-3 col-form-label text-md-right">{{ $t('confirm_password') }}</label>
<div class="col-md-7">
<input v-model="form.password_confirmation" :class="{ 'is-invalid': form.errors.has('password_confirmation') }" class="form-control" type="password" name="password_confirmation">
<has-error :form="form" field="password_confirmation"/>
</div>
</div>
<div class="form-group row">
<div class="col-md-7 offset-md-3 d-flex">
<!-- Submit Button -->
<v-button :loading="form.busy">
{{ $t('register') }}
</v-button>
<!-- GitHub Register Button -->
<login-with-github/>
</div>
</div>
</form>
</card>
</div>
</div>
</template>
<script>
import Form from 'vform'
import LoginWithGithub from '~/components/LoginWithGithub'
export default {
middleware: 'guest',
components: {
LoginWithGithub
},
metaInfo () {
return { title: this.$t('register') }
},
data: () => ({
form: new Form({
name: '',
email: '',
password: '',
password_confirmation: ''
})
}),
methods: {
async register () {
// Register the user.
const { data } = await this.form.post('/api/register')
// Log in the user.
const { data: { token } } = await this.form.post('/api/login')
// Save the token.
this.$store.dispatch('auth/saveToken', { token })
// Update the user.
await this.$store.dispatch('auth/updateUser', { user: data })
// Redirect home.
if(store.getters['auth/user'].role === 'admin'){
this.$router.push({ name: 'admin.home' })
}
this.$router.push({ name: 'home' })
}
}
}
</script>
+17
View File
@@ -0,0 +1,17 @@
<template>
<card class="text-center">
<h3 class="mb-4">{{ $t('page_not_found') }}</h3>
<div class="links">
<router-link :to="{ name: 'welcome' }">
{{ $t('go_home') }}
</router-link>
</div>
</card>
</template>
<script>
export default {
name: 'NotFound'
}
</script>
+15
View File
@@ -0,0 +1,15 @@
<template>
<card :title="$t('home')">
{{ $t('you_are_logged_in') }}
</card>
</template>
<script>
export default {
middleware: ['auth', 'admin'],
metaInfo () {
return { title: this.$t('home') }
}
}
</script>
@@ -0,0 +1,51 @@
<template>
<div class="row">
<div class="col-md-3">
<card :title="$t('settings')" class="settings-card">
<ul class="nav flex-column nav-pills">
<li v-for="tab in tabs" :key="tab.route" class="nav-item">
<router-link :to="{ name: tab.route }" class="nav-link" active-class="active">
<fa :icon="tab.icon" fixed-width/>
{{ tab.name }}
</router-link>
</li>
</ul>
</card>
</div>
<div class="col-md-9">
<transition name="fade" mode="out-in">
<router-view/>
</transition>
</div>
</div>
</template>
<script>
export default {
middleware: 'auth',
computed: {
tabs () {
return [
{
icon: 'user',
name: this.$t('profile'),
route: 'settings.profile'
},
{
icon: 'lock',
name: this.$t('password'),
route: 'settings.password'
}
]
}
}
}
</script>
<style>
.settings-card .card-body {
padding: 0;
}
</style>
@@ -0,0 +1,59 @@
<template>
<card :title="$t('your_password')">
<form @submit.prevent="update" @keydown="form.onKeydown($event)">
<alert-success :form="form" :message="$t('password_updated')"/>
<!-- Password -->
<div class="form-group row">
<label class="col-md-3 col-form-label text-md-right">{{ $t('new_password') }}</label>
<div class="col-md-7">
<input v-model="form.password" :class="{ 'is-invalid': form.errors.has('password') }" class="form-control" type="password" name="password">
<has-error :form="form" field="password"/>
</div>
</div>
<!-- Password Confirmation -->
<div class="form-group row">
<label class="col-md-3 col-form-label text-md-right">{{ $t('confirm_password') }}</label>
<div class="col-md-7">
<input v-model="form.password_confirmation" :class="{ 'is-invalid': form.errors.has('password_confirmation') }" class="form-control" type="password" name="password_confirmation">
<has-error :form="form" field="password_confirmation"/>
</div>
</div>
<!-- Submit Button -->
<div class="form-group row">
<div class="col-md-9 ml-md-auto">
<v-button :loading="form.busy" type="success">{{ $t('update') }}</v-button>
</div>
</div>
</form>
</card>
</template>
<script>
import Form from 'vform'
export default {
scrollToTop: false,
metaInfo () {
return { title: this.$t('settings') }
},
data: () => ({
form: new Form({
password: '',
password_confirmation: ''
})
}),
methods: {
async update () {
await this.form.patch('/api/settings/password')
this.form.reset()
}
}
}
</script>
@@ -0,0 +1,71 @@
<template>
<card :title="$t('your_info')">
<form @submit.prevent="update" @keydown="form.onKeydown($event)">
<alert-success :form="form" :message="$t('info_updated')"/>
<!-- Name -->
<div class="form-group row">
<label class="col-md-3 col-form-label text-md-right">{{ $t('name') }}</label>
<div class="col-md-7">
<input v-model="form.name" :class="{ 'is-invalid': form.errors.has('name') }" class="form-control" type="text" name="name">
<has-error :form="form" field="name"/>
</div>
</div>
<!-- Email -->
<div class="form-group row">
<label class="col-md-3 col-form-label text-md-right">{{ $t('email') }}</label>
<div class="col-md-7">
<input v-model="form.email" :class="{ 'is-invalid': form.errors.has('email') }" class="form-control" type="email" name="email">
<has-error :form="form" field="email" />
</div>
</div>
<!-- Submit Button -->
<div class="form-group row">
<div class="col-md-9 ml-md-auto">
<v-button :loading="form.busy" type="success">{{ $t('update') }}</v-button>
</div>
</div>
</form>
</card>
</template>
<script>
import Form from 'vform'
import { mapGetters } from 'vuex'
export default {
scrollToTop: false,
metaInfo () {
return { title: this.$t('settings') }
},
data: () => ({
form: new Form({
name: '',
email: ''
})
}),
computed: mapGetters({
user: 'auth/user'
}),
created () {
// Fill the form with user data.
this.form.keys().forEach(key => {
this.form[key] = this.user[key]
})
},
methods: {
async update () {
const { data } = await this.form.patch('/api/settings/profile')
this.$store.dispatch('auth/updateUser', { user: data })
}
}
}
</script>
+66
View File
@@ -0,0 +1,66 @@
<template>
<div>
<div class="top-right links">
<template v-if="authenticated">
<router-link :to="{ name: 'home' }">
{{ $t('home') }}
</router-link>
</template>
<template v-else>
<router-link :to="{ name: 'login' }">
{{ $t('login') }}
</router-link>
<router-link :to="{ name: 'register' }">
{{ $t('register') }}
</router-link>
</template>
</div>
<div class="text-center">
<div class="title mb-4">
{{ title }}
</div>
<div class="links">
<a href="https://laravel.com/docs">Documentation</a>
<a href="https://laracasts.com">Laracasts</a>
<a href="https://laravel-news.com">News</a>
<a href="https://forge.laravel.com">Forge</a>
<a href="https://github.com/laravel/laravel">GitHub</a>
</div>
</div>
</div>
</template>
<script>
import { mapGetters } from 'vuex'
export default {
middleware: 'guest',
layout: 'basic',
metaInfo () {
return { title: this.$t('home') }
},
data: () => ({
title: window.config.appName
}),
computed: mapGetters({
authenticated: 'auth/check'
})
}
</script>
<style scoped>
.top-right {
position: absolute;
right: 10px;
top: 18px;
}
.title {
font-size: 85px;
}
</style>
+55
View File
@@ -0,0 +1,55 @@
import axios from 'axios'
import store from '~/store'
import router from '~/router'
import swal from 'sweetalert2'
import i18n from '~/plugins/i18n'
// Request interceptor
axios.interceptors.request.use(request => {
const token = store.getters['auth/token']
if (token) {
request.headers.common['Authorization'] = `Bearer ${token}`
}
const locale = store.getters['lang/locale']
if (locale) {
request.headers.common['Accept-Language'] = locale
}
// request.headers['X-Socket-Id'] = Echo.socketId()
return request
})
// Response interceptor
axios.interceptors.response.use(response => response, error => {
const { status } = error.response
if (status >= 500) {
swal({
type: 'error',
title: i18n.t('error_alert_title'),
text: i18n.t('error_alert_text'),
reverseButtons: true,
confirmButtonText: i18n.t('ok'),
cancelButtonText: i18n.t('cancel')
})
}
if (status === 401 && store.getters['auth/check']) {
swal({
type: 'warning',
title: i18n.t('token_expired_alert_title'),
text: i18n.t('token_expired_alert_text'),
reverseButtons: true,
confirmButtonText: i18n.t('ok'),
cancelButtonText: i18n.t('cancel')
}).then(async () => {
await store.dispatch('auth/logout')
router.push({ name: 'login' })
})
}
return Promise.reject(error)
})
@@ -0,0 +1,19 @@
import Vue from 'vue'
import fontawesome from '@fortawesome/fontawesome'
import FontAwesomeIcon from '@fortawesome/vue-fontawesome'
// import { } from '@fortawesome/fontawesome-free-regular/shakable.es'
import {
faUser, faLock, faSignOutAlt, faCog
} from '@fortawesome/fontawesome-free-solid/shakable.es'
import {
faGithub
} from '@fortawesome/fontawesome-free-brands/shakable.es'
fontawesome.library.add(
faUser, faLock, faSignOutAlt, faCog, faGithub
)
Vue.component('fa', FontAwesomeIcon)
+30
View File
@@ -0,0 +1,30 @@
import Vue from 'vue'
import store from '~/store'
import VueI18n from 'vue-i18n'
Vue.use(VueI18n)
const i18n = new VueI18n({
locale: 'en',
messages: {}
})
/**
* @param {String} locale
*/
export async function loadMessages (locale) {
if (Object.keys(i18n.getLocaleMessage(locale)).length === 0) {
const messages = await import(/* webpackChunkName: "lang-[request]" */ `~/lang/${locale}`)
i18n.setLocaleMessage(locale, messages)
}
if (i18n.locale !== locale) {
i18n.locale = locale
}
}
;(async function () {
await loadMessages(store.getters['lang/locale'])
})()
export default i18n
+3
View File
@@ -0,0 +1,3 @@
import './axios'
import './fontawesome'
import 'bootstrap'
+199
View File
@@ -0,0 +1,199 @@
import Vue from 'vue'
import store from '~/store'
import Meta from 'vue-meta'
import routes from './routes'
import Router from 'vue-router'
import { sync } from 'vuex-router-sync'
Vue.use(Meta)
Vue.use(Router)
// The middleware for every page of the application.
const globalMiddleware = ['locale', 'check-auth']
// Load middleware modules dynamically.
const routeMiddleware = resolveMiddleware(
require.context('~/middleware', false, /.*\.js$/)
)
const router = createRouter()
sync(store, router)
export default router
/**
* Create a new router instance.
*
* @return {Router}
*/
function createRouter () {
const router = new Router({
scrollBehavior,
mode: 'history',
routes
})
router.beforeEach(beforeEach)
router.afterEach(afterEach)
return router
}
/**
* Global router guard.
*
* @param {Route} to
* @param {Route} from
* @param {Function} next
*/
async function beforeEach (to, from, next) {
// Get the matched components and resolve them.
const components = await resolveComponents(
router.getMatchedComponents({ ...to })
)
if (components.length === 0) {
return next()
}
// Start the loading bar.
if (components[components.length - 1].loading !== false) {
router.app.$nextTick(() => router.app.$loading.start())
}
// Get the middleware for all the matched components.
const middleware = getMiddleware(components)
// Call each middleware.
callMiddleware(middleware, to, from, (...args) => {
// Set the application layout only if "next()" was called with no args.
if (args.length === 0) {
router.app.setLayout(components[0].layout || '')
}
next(...args)
})
}
/**
* Global after hook.
*
* @param {Route} to
* @param {Route} from
* @param {Function} next
*/
async function afterEach (to, from, next) {
await router.app.$nextTick()
router.app.$loading.finish()
}
/**
* Call each middleware.
*
* @param {Array} middleware
* @param {Route} to
* @param {Route} from
* @param {Function} next
*/
function callMiddleware (middleware, to, from, next) {
const stack = middleware.reverse()
const _next = (...args) => {
// Stop if "_next" was called with an argument or the stack is empty.
if (args.length > 0 || stack.length === 0) {
if (args.length > 0) {
router.app.$loading.finish()
}
return next(...args)
}
const middleware = stack.pop()
if (typeof middleware === 'function') {
middleware(to, from, _next)
} else if (routeMiddleware[middleware]) {
routeMiddleware[middleware](to, from, _next)
} else {
throw Error(`Undefined middleware [${middleware}]`)
}
}
_next()
}
/**
* Resolve async components.
*
* @param {Array} components
* @return {Array}
*/
function resolveComponents (components) {
return Promise.all(components.map(component => {
return typeof component === 'function' ? component() : component
}))
}
/**
* Merge the the global middleware with the components middleware.
*
* @param {Array} components
* @return {Array}
*/
function getMiddleware (components) {
const middleware = [...globalMiddleware]
components.filter(c => c.middleware).forEach(component => {
if (Array.isArray(component.middleware)) {
middleware.push(...component.middleware)
} else {
middleware.push(component.middleware)
}
})
return middleware
}
/**
* Scroll Behavior
*
* @link https://router.vuejs.org/en/advanced/scroll-behavior.html
*
* @param {Route} to
* @param {Route} from
* @param {Object|undefined} savedPosition
* @return {Object}
*/
function scrollBehavior (to, from, savedPosition) {
if (savedPosition) {
return savedPosition
}
if (to.hash) {
return { selector: to.hash }
}
const [component] = router.getMatchedComponents({ ...to }).slice(-1)
if (component && component.scrollToTop === false) {
return {}
}
return { x: 0, y: 0 }
}
/**
* @param {Object} requireContext
* @return {Object}
*/
function resolveMiddleware (requireContext) {
return requireContext.keys()
.map(file =>
[file.replace(/(^.\/)|(\.js$)/g, ''), requireContext(file)]
)
.reduce((guards, [name, guard]) => (
{ ...guards, [name]: guard.default }
), {})
}
+35
View File
@@ -0,0 +1,35 @@
const Welcome = () => import('~/pages/welcome').then(m => m.default || m)
const Login = () => import('~/pages/auth/login').then(m => m.default || m)
const Register = () => import('~/pages/auth/register').then(m => m.default || m)
const PasswordEmail = () => import('~/pages/auth/password/email').then(m => m.default || m)
const PasswordReset = () => import('~/pages/auth/password/reset').then(m => m.default || m)
const NotFound = () => import('~/pages/errors/404').then(m => m.default || m)
const Home = () => import('~/pages/home').then(m => m.default || m)
const Settings = () => import('~/pages/settings/index').then(m => m.default || m)
const SettingsProfile = () => import('~/pages/settings/profile').then(m => m.default || m)
const SettingsPassword = () => import('~/pages/settings/password').then(m => m.default || m)
const AdminHome = () => import('~/pages/admin/home').then(m => m.default || m)
export default [
{ path: '/', name: 'welcome', component: Welcome },
{ path: '/login', name: 'login', component: Login },
{ path: '/register', name: 'register', component: Register },
{ path: '/password/reset', name: 'password.request', component: PasswordEmail },
{ path: '/password/reset/:token', name: 'password.reset', component: PasswordReset },
{ path: '/home', name: 'home', component: Home },
{ path: '/settings',
component: Settings,
children: [
{ path: '', redirect: { name: 'settings.profile' } },
{ path: 'profile', name: 'settings.profile', component: SettingsProfile },
{ path: 'password', name: 'settings.password', component: SettingsPassword }
] },
{ path: '/admin', name: 'admin.home', component: AdminHome },
{ path: '*', component: NotFound }
]
+23
View File
@@ -0,0 +1,23 @@
import Vue from 'vue'
import Vuex from 'vuex'
Vue.use(Vuex)
// Load store modules dynamically.
const requireContext = require.context('./modules', false, /.*\.js$/)
const modules = requireContext.keys()
.map(file =>
[file.replace(/(^.\/)|(\.js$)/g, ''), requireContext(file)]
)
.reduce((modules, [name, module]) => {
if (module.namespaced === undefined) {
module.namespaced = true
}
return { ...modules, [name]: module }
}, {})
export default new Vuex.Store({
modules
})
+79
View File
@@ -0,0 +1,79 @@
import axios from 'axios'
import Cookies from 'js-cookie'
import * as types from '../mutation-types'
// state
export const state = {
user: null,
token: Cookies.get('token')
}
// getters
export const getters = {
user: state => state.user,
token: state => state.token,
check: state => state.user !== null
}
// mutations
export const mutations = {
[types.SAVE_TOKEN] (state, { token, remember }) {
state.token = token
Cookies.set('token', token, { expires: remember ? 365 : null })
},
[types.FETCH_USER_SUCCESS] (state, { user }) {
state.user = user
},
[types.FETCH_USER_FAILURE] (state) {
state.token = null
Cookies.remove('token')
},
[types.LOGOUT] (state) {
state.user = null
state.token = null
Cookies.remove('token')
},
[types.UPDATE_USER] (state, { user }) {
state.user = user
}
}
// actions
export const actions = {
saveToken ({ commit, dispatch }, payload) {
commit(types.SAVE_TOKEN, payload)
},
async fetchUser ({ commit }) {
try {
const { data } = await axios.get('/api/user')
commit(types.FETCH_USER_SUCCESS, { user: data })
} catch (e) {
commit(types.FETCH_USER_FAILURE)
}
},
updateUser ({ commit }, payload) {
commit(types.UPDATE_USER, payload)
},
async logout ({ commit }) {
try {
await axios.post('/api/logout')
} catch (e) { }
commit(types.LOGOUT)
},
async fetchOauthUrl (ctx, { provider }) {
const { data } = await axios.post(`/api/oauth/${provider}`)
return data.url
}
}
+32
View File
@@ -0,0 +1,32 @@
import Cookies from 'js-cookie'
import * as types from '../mutation-types'
const { locale, locales } = window.config
// state
export const state = {
locale: Cookies.get('locale') || locale,
locales: locales
}
// getters
export const getters = {
locale: state => state.locale,
locales: state => state.locales
}
// mutations
export const mutations = {
[types.SET_LOCALE] (state, { locale }) {
state.locale = locale
}
}
// actions
export const actions = {
setLocale ({ commit }, { locale }) {
commit(types.SET_LOCALE, { locale })
Cookies.set('locale', locale, { expires: 365 })
}
}
@@ -0,0 +1,10 @@
// auth.js
export const LOGOUT = 'LOGOUT'
export const SAVE_TOKEN = 'SAVE_TOKEN'
export const FETCH_USER = 'FETCH_USER'
export const FETCH_USER_SUCCESS = 'FETCH_USER_SUCCESS'
export const FETCH_USER_FAILURE = 'FETCH_USER_FAILURE'
export const UPDATE_USER = 'UPDATE_USER'
// lang.js
export const SET_LOCALE = 'SET_LOCALE'
+14 -6
View File
@@ -1,8 +1,16 @@
// // Body
$body-bg: #f7f9fb;
// Body
$body-bg: #f5f8fa;
// Cards
$card-spacer-x: 0.9375rem;
$card-spacer-y: 0.625rem;
$card-cap-bg: #fbfbfb;
$card-border-color: #e8eced;
// Typography
$font-family-sans-serif: "Raleway", sans-serif;
$font-size-base: 0.9rem;
$line-height-base: 1.6;
// Borders
$border-radius: .125rem;
$border-radius-lg: .2rem;
$border-radius-sm: .15rem;
// Nav Pills
$nav-pills-border-radius: 0;
+6 -12
View File
@@ -1,14 +1,8 @@
// Fonts
@import url("https://fonts.googleapis.com/css?family=Raleway:300,400,600");
// Variables
@import "variables";
// Bootstrap
@import 'variables';
@import '~bootstrap/scss/bootstrap';
@import '~sweetalert2/src/sweetalert2';
.navbar-laravel {
background-color: #fff;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.04);
}
@import 'elements/card';
@import 'elements/navbar';
@import 'elements/buttons';
@import 'elements/transitions';
@@ -0,0 +1,25 @@
.btn-loading {
position: relative;
pointer-events: none;
color: transparent !important;
&:after {
animation: spinAround 500ms infinite linear;
border: 2px solid #dbdbdb;
border-radius: 50%;
border-right-color: transparent;
border-top-color: transparent;
content: "";
display: block;
height: 1em;
width: 1em;
position: absolute;
left: calc(50% - (1em / 2));
top: calc(50% - (1em / 2));
}
}
@keyframes spinAround {
from { transform: rotate(0deg); }
to { transform: rotate(359deg); }
}
@@ -0,0 +1,3 @@
.card {
box-shadow: 0 1px 1px rgba(0, 0, 0, 0.05);
}
@@ -0,0 +1,19 @@
.navbar {
font-weight: 600;
box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.1);
}
.nav-item {
.dropdown-menu {
border: none;
margin-top: .5rem;
border-top: 1px solid #f2f2f2 !important;
box-shadow: 0 4px 8px 0 rgba(0,0,0,0.12), 0 2px 4px 0 rgba(0,0,0,0.08);
}
}
.nav-link {
.svg-inline--fa {
font-size: 1.4rem;
}
}
@@ -0,0 +1,17 @@
.page-enter-active,
.page-leave-active {
transition: opacity .2s;
}
.page-enter,
.page-leave-to {
opacity: 0;
}
.fade-enter-active,
.fade-leave-active {
transition: opacity .15s
}
.fade-enter,
.fade-leave-to {
opacity: 0
}
-1
View File
@@ -65,7 +65,6 @@ return [
'array' => 'The :attribute must have at least :min items.',
],
'not_in' => 'The selected :attribute is invalid.',
'not_regex' => 'The :attribute format is invalid.',
'numeric' => 'The :attribute must be a number.',
'present' => 'The :attribute field must be present.',
'regex' => 'The :attribute format is invalid.',
+19
View File
@@ -0,0 +1,19 @@
<?php
return [
/*
|--------------------------------------------------------------------------
| Authentication Language Lines
|--------------------------------------------------------------------------
|
| The following language lines are used during authentication for various
| messages that we need to display to the user. You are free to modify
| these language lines according to your application's requirements.
|
*/
'failed' => 'Estas credenciales no coinciden con nuestros registros.',
'throttle' => 'Demasiados intentos de acceso. Por favor intente nuevamente en :seconds segundos.',
];
+19
View File
@@ -0,0 +1,19 @@
<?php
return [
/*
|--------------------------------------------------------------------------
| Pagination Language Lines
|--------------------------------------------------------------------------
|
| The following language lines are used by the paginator library to build
| the simple pagination links. You are free to change them to anything
| you want to customize your views to better match your application.
|
*/
'previous' => '&laquo; Anterior',
'next' => 'Siguiente &raquo;',
];
+22
View File
@@ -0,0 +1,22 @@
<?php
return [
/*
|--------------------------------------------------------------------------
| Password Reminder Language Lines
|--------------------------------------------------------------------------
|
| The following language lines are the default lines which match reasons
| that are given by the password broker for a password update attempt
| has failed, such as for an invalid token or invalid new password.
|
*/
'password' => 'Las contraseñas deben coincidir y contener al menos 6 caracteres',
'reset' => '¡Tu contraseña ha sido restablecida!',
'sent' => '¡Te hemos enviado por correo el enlace para restablecer tu contraseña!',
'token' => 'El token de recuperación de contraseña es inválido.',
'user' => 'No podemos encontrar ningún usuario con ese correo electrónico.',
];
+155
View File
@@ -0,0 +1,155 @@
<?php
return [
/*
|--------------------------------------------------------------------------
| Validation Language Lines
|--------------------------------------------------------------------------
|
| The following language lines contain the default error messages used by
| the validator class. Some of these rules have multiple versions such
| as the size rules. Feel free to tweak each of these messages.
|
*/
'accepted' => ':attribute debe ser aceptado.',
'active_url' => ':attribute no es una URL válida.',
'after' => ':attribute debe ser una fecha posterior a :date.',
'after_or_equal' => ':attribute debe ser una fecha posterior o igual a :date.',
'alpha' => ':attribute sólo debe contener letras.',
'alpha_dash' => ':attribute sólo debe contener letras, números y guiones.',
'alpha_num' => ':attribute sólo debe contener letras y números.',
'array' => ':attribute debe ser un conjunto.',
'before' => ':attribute debe ser una fecha anterior a :date.',
'before_or_equal' => ':attribute debe ser una fecha anterior o igual a :date.',
'between' => [
'numeric' => ':attribute tiene que estar entre :min - :max.',
'file' => ':attribute debe pesar entre :min - :max kilobytes.',
'string' => ':attribute tiene que tener entre :min - :max caracteres.',
'array' => ':attribute tiene que tener entre :min - :max ítems.',
],
'boolean' => 'El campo :attribute debe tener un valor verdadero o falso.',
'confirmed' => 'La confirmación de :attribute no coincide.',
'date' => ':attribute no es una fecha válida.',
'date_format' => ':attribute no corresponde al formato :format.',
'different' => ':attribute y :other deben ser diferentes.',
'digits' => ':attribute debe tener :digits dígitos.',
'digits_between' => ':attribute debe tener entre :min y :max dígitos.',
'dimensions' => 'Las dimensiones de la imagen :attribute no son válidas.',
'distinct' => 'El campo :attribute contiene un valor duplicado.',
'email' => ':attribute no es un correo válido',
'exists' => ':attribute es inválido.',
'file' => 'El campo :attribute debe ser un archivo.',
'filled' => 'El campo :attribute es obligatorio.',
'image' => ':attribute debe ser una imagen.',
'in' => ':attribute es inválido.',
'in_array' => 'El campo :attribute no existe en :other.',
'integer' => ':attribute debe ser un número entero.',
'ip' => ':attribute debe ser una dirección IP válida.',
'ipv4' => ':attribute debe ser un dirección IPv4 válida',
'ipv6' => ':attribute debe ser un dirección IPv6 válida.',
'json' => 'El campo :attribute debe tener una cadena JSON válida.',
'max' => [
'numeric' => ':attribute no debe ser mayor a :max.',
'file' => ':attribute no debe ser mayor que :max kilobytes.',
'string' => ':attribute no debe ser mayor que :max caracteres.',
'array' => ':attribute no debe tener más de :max elementos.',
],
'mimes' => ':attribute debe ser un archivo con formato: :values.',
'mimetypes' => ':attribute debe ser un archivo con formato: :values.',
'min' => [
'numeric' => 'El tamaño de :attribute debe ser de al menos :min.',
'file' => 'El tamaño de :attribute debe ser de al menos :min kilobytes.',
'string' => ':attribute debe contener al menos :min caracteres.',
'array' => ':attribute debe tener al menos :min elementos.',
],
'not_in' => ':attribute es inválido.',
'numeric' => ':attribute debe ser numérico.',
'present' => 'El campo :attribute debe estar presente.',
'regex' => 'El formato de :attribute es inválido.',
'required' => 'El campo :attribute es obligatorio.',
'required_if' => 'El campo :attribute es obligatorio cuando :other es :value.',
'required_unless' => 'El campo :attribute es obligatorio a menos que :other esté en :values.',
'required_with' => 'El campo :attribute es obligatorio cuando :values está presente.',
'required_with_all' => 'El campo :attribute es obligatorio cuando :values está presente.',
'required_without' => 'El campo :attribute es obligatorio cuando :values no está presente.',
'required_without_all' => 'El campo :attribute es obligatorio cuando ninguno de :values estén presentes.',
'same' => ':attribute y :other deben coincidir.',
'size' => [
'numeric' => 'El tamaño de :attribute debe ser :size.',
'file' => 'El tamaño de :attribute debe ser :size kilobytes.',
'string' => ':attribute debe contener :size caracteres.',
'array' => ':attribute debe contener :size elementos.',
],
'string' => 'El campo :attribute debe ser una cadena de caracteres.',
'timezone' => 'El :attribute debe ser una zona válida.',
'unique' => ':attribute ya ha sido registrado.',
'uploaded' => 'Subir :attribute ha fallado.',
'url' => 'El formato :attribute es inválido.',
/*
|--------------------------------------------------------------------------
| Custom Validation Language Lines
|--------------------------------------------------------------------------
|
| Here you may specify custom validation messages for attributes using the
| convention "attribute.rule" to name the lines. This makes it quick to
| specify a specific custom language line for a given attribute rule.
|
*/
'custom' => [
'password' => [
'min' => 'La :attribute debe contener más de :min caracteres',
],
'email' => [
'unique' => 'El :attribute ya ha sido registrado.',
],
],
/*
|--------------------------------------------------------------------------
| Custom Validation Attributes
|--------------------------------------------------------------------------
|
| The following language lines are used to swap attribute place-holders
| with something more reader friendly such as E-Mail Address instead
| of "email". This simply helps us make messages a little cleaner.
|
*/
'attributes' => [
'name' => 'nombre',
'username' => 'usuario',
'email' => 'correo electrónico',
'first_name' => 'nombre',
'last_name' => 'apellido',
'password' => 'contraseña',
'password_confirmation' => 'confirmación de la contraseña',
'city' => 'ciudad',
'country' => 'país',
'address' => 'dirección',
'phone' => 'teléfono',
'mobile' => 'móvil',
'age' => 'edad',
'sex' => 'sexo',
'gender' => 'género',
'year' => 'año',
'month' => 'mes',
'day' => 'día',
'hour' => 'hora',
'minute' => 'minuto',
'second' => 'segundo',
'title' => 'título',
'content' => 'contenido',
'body' => 'contenido',
'description' => 'descripción',
'excerpt' => 'extracto',
'date' => 'fecha',
'time' => 'hora',
'subject' => 'asunto',
'message' => 'mensaje',
],
];
+16
View File
@@ -0,0 +1,16 @@
<?php
return [
/*
|--------------------------------------------------------------------------
| Authentication Language Lines
|--------------------------------------------------------------------------
|
| The following language lines are used during authentication for various
| messages that we need to display to the user. You are free to modify
| these language lines according to your application's requirements.
|
*/
'failed' => '用户名或手机号与密码不匹配或用户被禁用',
'throttle' => '失败次数太多,请在:seconds秒后再尝试',
];
+16
View File
@@ -0,0 +1,16 @@
<?php
return [
/*
|--------------------------------------------------------------------------
| Pagination Language Lines
|--------------------------------------------------------------------------
|
| The following language lines are used by the paginator library to build
| the simple pagination links. You are free to change them to anything
| you want to customize your views to better match your application.
|
*/
'previous' => '&laquo; 上一页',
'next' => '下一页 &raquo;',
];
+19
View File
@@ -0,0 +1,19 @@
<?php
return [
/*
|--------------------------------------------------------------------------
| Password Reset Language Lines
|--------------------------------------------------------------------------
|
| The following language lines are the default lines which match reasons
| that are given by the password broker for a password update attempt
| has failed, such as for an invalid token or invalid new password.
|
*/
'password' => '密码长度至少包含6个字符并且两次输入密码要一致',
'reset' => '密码已经被重置!',
'sent' => '我们已经发送密码重置链接到您的邮箱',
'token' => '密码重置令牌无效',
'user' => '抱歉,该邮箱对应的用户不存在!',
];
+97
View File
@@ -0,0 +1,97 @@
<?php
return [
/*
|--------------------------------------------------------------------------
| Validation Language Lines
|--------------------------------------------------------------------------
|
| The following language lines contain the default error messages used by
| the validator class. Some of these rules have multiple versions such
| as the size rules. Feel free to tweak each of these messages here.
|
*/
'unique' => ':attribute 已存在',
'accepted' => ':attribute 是被接受的',
'active_url' => ':attribute 必须是一个合法的 URL',
'after' => ':attribute 必须是 :date 之后的一个日期',
'alpha' => ':attribute 必须全部由字母字符构成。',
'alpha_dash' => ':attribute 必须全部由字母、数字、中划线或下划线字符构成',
'alpha_num' => ':attribute 必须全部由字母和数字构成',
'array' => ':attribute 必须是个数组',
'before' => ':attribute 必须是 :date 之前的一个日期',
'between' => [
'numeric' => ':attribute 必须在 :min 到 :max 之间',
'file' => ':attribute 必须在 :min 到 :max KB之间',
'string' => ':attribute 必须在 :min 到 :max 个字符之间',
'array' => ':attribute 必须在 :min 到 :max 项之间',
],
'boolean' => ':attribute 字符必须是 true 或 false',
'confirmed' => ':attribute 两次确认不匹配',
'date' => ':attribute 必须是一个合法的日期',
'date_format' => ':attribute 与给定的格式 :format 不符合',
'different' => ':attribute 必须不同于:other',
'digits' => ':attribute 必须是 :digits 位',
'digits_between' => ':attribute 必须在 :min and :max 位之间',
'email' => ':attribute 必须是一个合法的电子邮件地址。',
'filled' => ':attribute 的字段是必填的',
'exists' => '选定的 :attribute 是无效的',
'image' => ':attribute 必须是一个图片 (jpeg, png, bmp 或者 gif)',
'in' => '选定的 :attribute 是无效的',
'integer' => ':attribute 必须是个整数',
'ip' => ':attribute 必须是一个合法的 IP 地址。',
'max' => [
'numeric' => ':attribute 的最大长度为 :max 位',
'file' => ':attribute 的最大为 :max',
'string' => ':attribute 的最大长度为 :max 字符',
'array' => ':attribute 的最大个数为 :max 个',
],
'mimes' => ':attribute 的文件类型必须是:values',
'mimetypes' => ':attribute 的文件类型必须是: :values.',
'min' => [
'numeric' => ':attribute 的最小长度为 :min 位',
'string' => ':attribute 的最小长度为 :min 字符',
'file' => ':attribute 大小至少为:min KB',
'array' => ':attribute 至少有 :min 项',
],
'not_in' => '选定的 :attribute 是无效的',
'numeric' => ':attribute 必须是数字',
'regex' => ':attribute 格式是无效的',
'required' => ':attribute 字段必须填写',
'required_if' => ':attribute 字段是必须的当 :other 是 :value',
'required_with' => ':attribute 字段是必须的当 :values 是存在的',
'required_with_all' => ':attribute 字段是必须的当 :values 是存在的',
'required_without' => ':attribute 字段是必须的当 :values 是不存在的',
'required_without_all' => ':attribute 字段是必须的当 没有一个 :values 是存在的',
'same' => ':attribute 和 :other 必须匹配',
'size' => [
'numeric' => ':attribute 必须是 :size 位',
'file' => ':attribute 必须是 :size KB',
'string' => ':attribute 必须是 :size 个字符',
'array' => ':attribute 必须包括 :size 项',
],
'url' => ':attribute 无效的格式',
'timezone' => ':attribute 必须个有效的时区',
/*
|--------------------------------------------------------------------------
| Custom Validation Language Lines
|--------------------------------------------------------------------------
|
| Here you may specify custom validation messages for attributes using the
| convention "attribute.rule" to name the lines. This makes it quick to
| specify a specific custom language line for a given attribute rule.
|
*/
'custom' => [],
/*
|--------------------------------------------------------------------------
| Custom Validation Attributes
|--------------------------------------------------------------------------
|
| The following language lines are used to swap attribute place-holders
| with something more reader friendly such as E-Mail Address instead
| of "email". This simply helps us make messages a little cleaner.
|
*/
'attributes' => [],
];
-22
View File
@@ -1,22 +0,0 @@
<!DOCTYPE html>
<html lang="en-US">
<head>
<meta charset="utf-8">
</head>
<body>
<div>
Hi {{ $name }},
<br>
Thank you for creating an account with us. Don't forget to complete your registration!
<br>
Please click on the link below or copy it into the address bar of your browser to confirm your email address:
<br>
<a href="{{ url('user/verify', $verification_code)}}">Confirm my email address </a>
<br/>
</div>
</body>
</html>
+58
View File
@@ -0,0 +1,58 @@
{{-- Illuminate/Foundation/Exceptions/views --}}
<!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">
<title>@yield('title')</title>
<!-- Fonts -->
<link href="https://fonts.googleapis.com/css?family=Raleway:100,600" rel="stylesheet" type="text/css">
<!-- Styles -->
<style>
html, body {
background-color: #fff;
color: #636b6f;
font-family: 'Raleway', sans-serif;
font-weight: 100;
height: 100vh;
margin: 0;
}
.full-height {
height: 100vh;
}
.flex-center {
align-items: center;
display: flex;
justify-content: center;
}
.position-ref {
position: relative;
}
.content {
text-align: center;
}
.title {
font-size: 36px;
padding: 20px;
}
</style>
</head>
<body>
<div class="flex-center position-ref full-height">
<div class="content">
<div class="title">
@yield('message')
</div>
</div>
</div>
</body>
</html>
+49
View File
@@ -0,0 +1,49 @@
@php
$config = [
'appName' => config('app.name'),
'locale' => $locale = app()->getLocale(),
'locales' => config('app.locales'),
'githubAuth' => config('services.github.client_id'),
];
$polyfills = [
'Promise',
'Object.assign',
'Object.values',
'Array.prototype.find',
'Array.prototype.findIndex',
'Array.prototype.includes',
'String.prototype.includes',
'String.prototype.startsWith',
'String.prototype.endsWith',
];
@endphp
<!DOCTYPE html>
<html lang="{{ app()->getLocale() }}">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<title>{{ config('app.name') }}</title>
<link rel="stylesheet" href="{{ mix('css/app.css') }}">
</head>
<body>
<div id="app"></div>
{{-- Global configuration object --}}
<script>window.config = @json($config);</script>
{{-- Polyfill JS features via polyfill.io --}}
<script src="https://cdn.polyfill.io/v2/polyfill.min.js?features={{ implode(',', $polyfills) }}"></script>
{{-- Load the application scripts --}}
@if (app()->isLocal())
<script src="{{ mix('js/app.js') }}"></script>
@else
<script src="{{ mix('js/manifest.js') }}"></script>
<script src="{{ mix('js/vendor.js') }}"></script>
<script src="{{ mix('js/app.js') }}"></script>
@endif
</body>
</html>
+12
View File
@@ -0,0 +1,12 @@
<html>
<head>
<meta charset="utf-8">
<title>{{ config('app.name') }}</title>
<script>
window.opener.postMessage({ token: "{{ $token }}" }, "{{ url('/') }}")
window.close()
</script>
</head>
<body>
</body>
</html>
@@ -0,0 +1,5 @@
@extends('errors.layout')
@section('title', 'Login Error')
@section('message', 'Email already taken.')
-95
View File
@@ -1,95 +0,0 @@
<!doctype html>
<html lang="{{ app()->getLocale() }}">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Laravel</title>
<!-- Fonts -->
<link href="https://fonts.googleapis.com/css?family=Raleway:100,600" rel="stylesheet" type="text/css">
<!-- Styles -->
<style>
html, body {
background-color: #fff;
color: #636b6f;
font-family: 'Raleway', sans-serif;
font-weight: 100;
height: 100vh;
margin: 0;
}
.full-height {
height: 100vh;
}
.flex-center {
align-items: center;
display: flex;
justify-content: center;
}
.position-ref {
position: relative;
}
.top-right {
position: absolute;
right: 10px;
top: 18px;
}
.content {
text-align: center;
}
.title {
font-size: 84px;
}
.links > a {
color: #636b6f;
padding: 0 25px;
font-size: 12px;
font-weight: 600;
letter-spacing: .1rem;
text-decoration: none;
text-transform: uppercase;
}
.m-b-md {
margin-bottom: 30px;
}
</style>
</head>
<body>
<div class="flex-center position-ref full-height">
@if (Route::has('login'))
<div class="top-right links">
@auth
<a href="{{ url('/home') }}">Home</a>
@else
<a href="{{ route('login') }}">Login</a>
<a href="{{ route('register') }}">Register</a>
@endauth
</div>
@endif
<div class="content">
<div class="title m-b-md">
Laravel
</div>
<div class="links">
<a href="https://laravel.com/docs">Documentation</a>
<a href="https://laracasts.com">Laracasts</a>
<a href="https://laravel-news.com">News</a>
<a href="https://forge.laravel.com">Forge</a>
<a href="https://github.com/laravel/laravel">GitHub</a>
</div>
</div>
</div>
</body>
</html>