Keep session and added Topbar component

This commit is contained in:
Joan Sanchez 2022-03-02 15:29:41 +01:00
parent 5d6cdcb6df
commit 44b823e376
10 changed files with 270 additions and 177 deletions

22
src/assets/logo_icon.svg Normal file
View File

@ -0,0 +1,22 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:cc="http://creativecommons.org/ns#" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns:svg="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg" xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" width="40mm" height="40mm" viewBox="0 0 40 40" version="1.1" id="svg823" inkscape:version="0.92.4 (5da689c313, 2019-01-14)" sodipodi:docname="logo.svg">
<defs id="defs817"/>
<sodipodi:namedview id="base" pagecolor="#2f2f2f" bordercolor="#666666" borderopacity="1.0" inkscape:pageopacity="0" inkscape:pageshadow="2" inkscape:zoom="5.635625" inkscape:cx="70.551181" inkscape:cy="75.590551" inkscape:document-units="mm" inkscape:current-layer="layer1" showgrid="false" inkscape:window-width="1920" inkscape:window-height="1043" inkscape:window-x="1920" inkscape:window-y="0" inkscape:window-maximized="1"/>
<metadata id="metadata820">
<rdf:RDF>
<cc:Work rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type rdf:resource="http://purl.org/dc/dcmitype/StillImage"/>
<dc:title/>
</cc:Work>
</rdf:RDF>
</metadata>
<g inkscape:label="Layer 1" inkscape:groupmode="layer" id="layer1" transform="translate(0,-257)">
<path style="fill:#88bd32;stroke-width:0.83333331" inkscape:connector-curvature="0" id="path4" d="m 27.583333,261.08333 c 0.25,-0.0833 0.5,-0.16666 0.75,-0.16666 L 39,259.5 l -0.166667,6.08333 c -0.166666,5 -3.083333,9.5 -6.666666,10.5 -0.25,0.0833 -0.5,0.16667 -0.75,0.16667 L 20.75,277.66667 l 0.166667,-6.08334 c 0.166666,-5.08333 3.083333,-9.5 6.666666,-10.5 z" class="st0"/>
<path style="fill:#88bd32;stroke-width:0.83333331" inkscape:connector-curvature="0" id="path6" d="m 5.9166667,281.91667 c 0.1666667,-0.0833 0.4166667,-0.0833 0.5833334,-0.0833 L 14.25,280.75 14.16667,285.08333 C 14.08334,288.75 11.91667,292 9.3333364,292.75 c -0.25,0.0833 -0.4166667,0.0833 -0.5833334,0.0833 L 1.0000001,293.91667 1.0833334,289.5 c 0.1666667,-3.58333 2.25,-6.91667 4.8333333,-7.58333 z" class="st0"/>
<g id="g10" style="fill:#f7931e;fill-opacity:1;stroke:none;stroke-opacity:1" transform="matrix(0.83333333,0,0,0.83333333,8.0000002e-8,257)">
<path style="fill:#f7931e;fill-opacity:1;stroke:none;stroke-opacity:1" inkscape:connector-curvature="0" id="path8" d="m 12,48 c -0.4,0 -0.7,-0.3 -0.7,-0.6 0,-0.4 0.2,-0.7 0.6,-0.8 0,0 3,-0.3 5.5,-3.4 3,-3.8 4.1,-10.1 3,-18.4 C 19.3,16.3 20.6,9.7 24.1,5.3 27.8,0.6 32.5,0 32.7,0 c 0.4,0 0.7,0.2 0.8,0.6 0,0.4 -0.2,0.7 -0.6,0.8 0,0 -4.3,0.6 -7.6,4.7 -3.3,4.2 -4.4,10.4 -3.4,18.5 1.1,8.8 0,15.4 -3.4,19.5 -2.8,3.5 -6.2,3.9 -6.5,3.9 0.1,0 0.1,0 0,0 z" class="st1"/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.8 KiB

146
src/components/Topbar.vue Normal file
View File

@ -0,0 +1,146 @@
<template>
<q-header class="bg-darker" color="white" elevated>
<q-toolbar class="q-py-sm q-px-md">
<q-btn flat @click="drawer = !drawer" round dense icon="menu" />
<router-link to="/">
<q-btn flat round class="q-ml-xs" v-if="$q.screen.gt.xs">
<q-avatar square size="md"><img src="@/assets/logo_icon.svg" alt="Logo" /></q-avatar>
</q-btn>
</router-link>
<q-toolbar-title shrink class="text-weight-bold"> Salix </q-toolbar-title>
<q-space></q-space>
<q-btn dense flat no-wrap>
<q-avatar size="lg">
<q-img
:src="`/api/Images/user/160x160/${user.id}/download?access_token=${token}`"
spinner-color="white"
/>
</q-avatar>
<q-tooltip>Account</q-tooltip>
<q-icon name="arrow_drop_down" size="s" />
<q-menu>
<div class="row no-wrap q-pa-md">
<div class="column">
<div class="text-h6 q-mb-md">Settings</div>
<q-toggle
:label="$t(`globals.lang['${language}']`)"
icon="public"
color="orange"
false-value="es"
true-value="en"
v-model="language"
/>
<q-toggle
v-model="mode"
checked-icon="dark_mode"
color="orange"
unchecked-icon="light_mode"
/>
<q-btn color="orange" outline size="sm" label="Settings" icon="settings" />
</div>
<q-separator vertical inset class="q-mx-lg" />
<div class="column items-center" style="min-width: 150px">
<q-avatar size="80px">
<q-img
:src="`/api/Images/user/160x160/${user.id}/download?access_token=${token}`"
spinner-color="white"
/>
</q-avatar>
<div class="text-subtitle1 q-mt-md">
<strong>{{ user.nickname }}</strong>
</div>
<div class="text-subtitle3 text-grey-7 q-mb-xs">@{{ user.username }}</div>
<q-btn
color="orange"
flat
label="Log Out"
size="sm"
icon="logout"
@click="logout()"
v-close-popup
/>
</div>
</div>
</q-menu>
</q-btn>
</q-toolbar>
</q-header>
</template>
<script lang="ts">
import { Options, Vue } from 'vue-class-component';
import axios from 'axios';
import { Dark } from 'quasar';
interface UserProfile {
id: number;
username: string;
nickname: string;
token: string;
}
@Options({
props: {
drawer: {
type: Boolean,
required: true,
},
},
})
export default class Topbar extends Vue {
drawer?: boolean;
private lang = 'es';
mounted(): void {
axios
.get('/api/accounts/acl')
.then((response) => {
this.$store.dispatch('updateUserData', response.data);
})
.catch(() => {
this.$q.notify({
message: this.$t('globals.accessDenied'),
type: 'negative',
});
this.$store.dispatch('logOut');
this.$router.push('/login');
});
}
logout(): void {
this.$store.dispatch('logOut');
this.$router.push('/login');
}
get user(): UserProfile {
return this.$store.state.user;
}
get token(): string {
return this.$store.getters.token;
}
get mode(): boolean {
return Dark.isActive;
}
set mode(value: boolean) {
Dark.set(value);
}
get language(): string {
return this.lang;
}
set language(value: string) {
this.lang = value;
this.$i18n.locale = value;
}
}
</script>

View File

View File

@ -3,7 +3,9 @@
"lang": {
"es": "Español",
"en": "Inglés"
}
},
"accessDenied": "Acceso denegado",
"internalServerError": "Ha ocurrido un error interno del servidor"
},
"login": {
"title": "Iniciar sesión",

View File

@ -1,4 +1,5 @@
import { createRouter, createWebHistory, RouteRecordRaw } from 'vue-router';
import store from '../store';
const routes: Array<RouteRecordRaw> = [
{
@ -12,15 +13,15 @@ const routes: Array<RouteRecordRaw> = [
component: () => import('../views/Main/Main.vue'),
children: [
{
path: '',
path: '/dashboard',
name: 'Dashboard',
component: () => import('../views/Main/Dashboard.vue'),
},
/* {
path: '/Client',
name: 'Client',
component: () => import('../views/Client/client.vue'),
}, */
{
path: '/customer',
name: 'Customer',
component: () => import('../views/Customer/Main.vue'),
},
{
path: '/:pathMatch(.*)*',
name: 'NotFound',
@ -36,7 +37,7 @@ const router = createRouter({
});
router.beforeEach((to, from, next) => {
const loggedIn = localStorage.getItem('token');
const loggedIn = store.getters.isLoggedIn;
if (to.name !== 'Login' && !loggedIn) {
next({ path: '/login', query: { redirect: to.fullPath } });

View File

@ -1,12 +1,26 @@
import { createStore } from 'vuex';
interface UserProfile {
id: number;
username: string;
nickname: string;
token: string;
}
export default createStore({
state: {
user: null,
user: {},
roles: [],
},
mutations: {
setUser(state, user) {
setUser(state, data) {
const user: UserProfile = {
id: data.id,
username: data.name,
nickname: data.nickname,
token: data.token,
};
state.user = user;
},
setRoles(state, roles) {
@ -14,15 +28,20 @@ export default createStore({
},
},
actions: {
logIn({ commit }, auth) {
localStorage.setItem('token', auth.token);
logIn({ commit }, data) {
if (data.keepLogin) {
localStorage.setItem('token', data.token);
} else {
sessionStorage.setItem('token', data.token);
}
commit('setUser', auth);
commit('setUser', data);
},
logOut({ commit }) {
localStorage.removeItem('token');
sessionStorage.getItem('token');
commit('setUser', null);
commit('setUser', {});
},
updateUserData({ commit }, data) {
commit('setUser', data.user);
@ -30,8 +49,17 @@ export default createStore({
},
},
getters: {
hasData(state) {
return !!state.user;
isLoggedIn() {
const localToken = localStorage.getItem('token');
const sessionToken = sessionStorage.getItem('token');
return !!(localToken || sessionToken);
},
token() {
const localToken = localStorage.getItem('token');
const sessionToken = sessionStorage.getItem('token');
return localToken || sessionToken;
},
},
modules: {},

View File

@ -0,0 +1,16 @@
<template>
<q-layout>
<q-page class="q-pa-md">
<q-card class="q-pa-md"> Dashboard page.. </q-card>
</q-page>
</q-layout>
</template>
<script lang="ts">
import { Options, Vue } from 'vue-class-component';
@Options({})
export default class Customer extends Vue {}
</script>
<style lang="scss" scoped></style>

View File

@ -55,7 +55,7 @@
</template>
<script lang="ts">
import { Vue } from 'vue-class-component';
import { Options, Vue } from 'vue-class-component';
import { Dark, useQuasar } from 'quasar';
import axios from 'axios';
@ -63,20 +63,21 @@ interface LoginForm {
username: string;
password: string;
keepLogin: boolean;
Dark: Dark;
}
@Options({
setup() {
return {
$q: useQuasar(),
};
},
})
export default class Login extends Vue {
username?: string;
password?: string;
keepLogin?: boolean;
private lang = 'es';
setup() {
return {
$q: useQuasar(),
};
}
get mode(): boolean {
return Dark.isActive;
}
@ -103,6 +104,7 @@ export default class Login extends Vue {
.then((response) => {
this.$store.dispatch('logIn', {
token: response.data.token,
keepLogin: this.keepLogin,
});
this.$q.notify({
@ -111,12 +113,20 @@ export default class Login extends Vue {
});
this.$router.push('/');
})
.catch(() =>
this.$q.notify({
message: this.$t('login.loginError'),
type: 'negative',
})
);
.catch((error) => {
const errorCode = error.response.status;
if (errorCode === 401) {
this.$q.notify({
message: this.$t('login.loginFailed'),
type: 'negative',
});
} else {
this.$q.notify({
message: this.$t('globals.internalServerError'),
type: 'negative',
});
}
});
}
data(): LoginForm {
@ -124,7 +134,6 @@ export default class Login extends Vue {
username: '',
password: '',
keepLogin: true,
Dark: Dark,
};
}
}

View File

@ -1,5 +1,9 @@
<template>
<q-layout> page.. </q-layout>
<q-layout>
<q-page class="q-pa-md">
<q-card class="q-pa-md"> Dashboard page.. </q-card>
</q-page>
</q-layout>
</template>
<script lang="ts">

View File

@ -1,73 +1,6 @@
<template>
<q-layout>
<q-header class="bg-darker" color="white" elevated>
<q-toolbar>
<q-btn flat @click="drawer = !drawer" round dense icon="menu" />
<q-toolbar-title>Salix</q-toolbar-title>
<q-space></q-space>
<q-btn dense flat no-wrap>
<q-avatar size="lg">
<q-img
:src="`/api/Images/user/160x160/${id}/download?access_token=${token}`"
spinner-color="white"
/>
</q-avatar>
<q-tooltip>Account</q-tooltip>
<q-icon name="arrow_drop_down" size="s" />
<q-menu>
<div class="row no-wrap q-pa-md">
<div class="column">
<div class="text-h6 q-mb-md">Settings</div>
<q-toggle
:label="$t(`globals.lang['${language}']`)"
icon="public"
color="orange"
false-value="es"
true-value="en"
v-model="language"
/>
<q-toggle
v-model="mode"
checked-icon="dark_mode"
color="orange"
unchecked-icon="light_mode"
/>
<q-btn color="orange" outline size="sm" label="Settings" icon="settings" />
</div>
<q-separator vertical inset class="q-mx-lg" />
<div class="column items-center" style="min-width: 150px">
<q-avatar size="80px">
<q-img
:src="`/api/Images/user/160x160/${id}/download?access_token=${token}`"
spinner-color="white"
/>
</q-avatar>
<div class="text-subtitle1 q-mt-md">
<strong>{{ nickname }}</strong>
</div>
<div class="text-subtitle3 text-grey-7 q-mb-xs">@{{ username }}</div>
<q-btn
color="orange"
flat
label="Log Out"
size="sm"
icon="logout"
@click="logout()"
v-close-popup
/>
</div>
</div>
</q-menu>
</q-btn>
</q-toolbar>
</q-header>
<Topbar :drawer="drawer" />
<q-drawer
v-model="drawer"
show-if-above
@ -80,21 +13,18 @@
>
<q-scroll-area class="fit">
<q-list padding>
<q-item active clickable v-ripple>
<q-item clickable v-ripple :to="{ path: '/dashboard' }" active-class="text-orange">
<q-item-section avatar>
<q-icon name="dashboard" />
</q-item-section>
<q-item-section> Dashboard </q-item-section>
<q-item-section> Dashboard</q-item-section>
</q-item>
<q-item clickable v-ripple>
<q-item clickable v-ripple :to="{ path: '/customer' }" active-class="text-orange">
<q-item-section avatar>
<q-icon name="people" />
</q-item-section>
<q-item-section> Customers </q-item-section>
</q-item>
<q-item clickable v-ripple>
<q-item-section avatar>
<q-icon name="receipt" />
@ -131,81 +61,16 @@
<script lang="ts">
import { Options, Vue } from 'vue-class-component';
import { Dark } from 'quasar';
import axios from 'axios';
import { ref } from 'vue';
import Topbar from '@/components/Topbar.vue';
@Options({})
@Options({
components: {
Topbar,
},
})
export default class Main extends Vue {
private lang = 'es';
drawer = false;
miniState = true;
mounted(): void {
axios.get('/api/accounts/acl').then((response) => {
this.$store.dispatch('updateUserData', response.data);
});
}
logout(): void {
this.$store.dispatch('logOut');
this.$router.push('/login');
}
get nickLetter(): string {
if (this.nickname) {
return this.nickname.charAt(0);
}
return '';
}
get nickname(): string {
if (this.$store.getters.hasData) {
return this.$store.state.user.nickname;
}
return '';
}
get username(): string {
if (this.$store.getters.hasData) {
return this.$store.state.user.name;
}
return '';
}
get id(): string {
if (this.$store.getters.hasData) {
return this.$store.state.user.id;
}
return '';
}
get token(): string {
const token = localStorage.getItem('token');
return token ? token : '';
}
get mode(): boolean {
return Dark.isActive;
}
set mode(value: boolean) {
Dark.set(value);
}
get language(): string {
return this.lang;
}
set language(value: string) {
this.lang = value;
this.$i18n.locale = value;
}
}
</script>