go-quasar-partial-ssr/frontend/src/layouts/MainLayout.vue

263 lines
6.9 KiB
Vue

<template>
<q-layout view="lHh Lpr lFf">
<q-header elevated>
<q-toolbar>
<img data-v-2373a833="" class="brand-logo-tb" :src="LogoUrl" alt="Omnimed logo">
<q-toolbar-title> Omnimed </q-toolbar-title>
<div>Quasar v{{ $q.version }}</div>
<div class="q-ml-md">
<q-btn
v-if="!currentUser"
flat
round
color="white"
icon="lock"
to="/login"
>
<q-tooltip>Login</q-tooltip>
</q-btn>
<q-btn v-else flat round dense>
<q-avatar size="34px" class="user-avatar">
<img v-if="currentUser.avatar" :src="currentUser.avatar" :alt="currentUser.name" />
<span v-else>{{ userInitials }}</span>
</q-avatar>
<q-menu anchor="bottom right" self="top right">
<q-list dense style="min-width: 190px">
<q-item>
<q-item-section>
<q-item-label>{{ currentUser.name }}</q-item-label>
<q-item-label caption>{{ currentUser.email }}</q-item-label>
</q-item-section>
</q-item>
<q-separator />
<q-item v-if="isAdmin" clickable v-close-popup to="/admin">
<q-item-section avatar>
<q-icon name="admin_panel_settings" />
</q-item-section>
<q-item-section>Admin</q-item-section>
</q-item>
<q-item clickable v-close-popup @click="logout">
<q-item-section avatar>
<q-icon name="logout" />
</q-item-section>
<q-item-section>Logout</q-item-section>
</q-item>
</q-list>
</q-menu>
</q-btn>
</div>
<div class="q-ml-md">
<q-select
v-model="model"
:options="options"
option-label="label"
option-value="value"
emit-value
map-options
class="q-select"
dark
>
<template v-slot:selected-item="scope">
<div class="row items-center no-wrap">
<img
class="border"
v-if="scope.opt.flagSrc"
:src="scope.opt.flagSrc"
:alt="scope.opt.label"
:style="scope.opt.flagStyle"
/>
<span v-else class="lang-fallback">{{ scope.opt.value.toUpperCase() }}</span>
<span class="bold text-white q-ml-md">{{ scope.opt.short_name }}</span>
</div>
</template>
<template v-slot:option="scope">
<q-item v-bind="scope.itemProps">
<q-item-section avatar>
<img
v-if="scope.opt.flagSrc"
:src="scope.opt.flagSrc"
:alt="scope.opt.label"
:style="scope.opt.flagStyle"
/>
<span v-else class="lang-fallback">{{ scope.opt.value.toUpperCase() }}</span>
</q-item-section>
<q-item-section>
<q-item-label>{{ scope.opt.label }}</q-item-label>
</q-item-section>
</q-item>
</template>
</q-select>
</div>
</q-toolbar>
</q-header>
<q-page-container>
<router-view />
</q-page-container>
</q-layout>
</template>
<script setup lang="ts">
import { computed, onMounted, ref, watch } from 'vue';
import { useI18n } from 'vue-i18n';
import { useRouter, useRoute } from 'vue-router';
import LogoUrl from 'src/assets/home/logo.png';
import { me, type UserShort } from 'src/api/api';
import { usePreferencesStore, type LanguageCode } from 'src/stores/preferences-store';
const { t } = useI18n();
const router = useRouter();
const route = useRoute();
const preferencesStore = usePreferencesStore();
const currentUser = ref<UserShort | null>(null);
const model = computed({
get: () => preferencesStore.language,
set: (language: LanguageCode) => {
preferencesStore.setLanguage(language);
},
});
const flagModules = import.meta.glob('../assets/flags/*', {
eager: true,
import: 'default',
});
const langs = [
{
code: 'it',
short_name: 'IT',
flag: 'it.svg',
style: 'width:32px;height:22px;',
},
{
code: 'en',
short_name: 'EN',
flag: 'en.svg',
style: 'width:32px;height:22px;',
},
{
code: 'en_us',
short_name: 'EN',
flag: 'en_us.svg',
style: 'width:32px;height:22px;',
},
{
code: 'de',
short_name: 'DE',
flag: 'de.svg',
style: 'width:32px;height:22px;',
},
{
code: 'de_ch',
short_name: 'DE',
flag: 'ch.svg',
style: 'width:22px;height:22px;object-fit: cover;',
},
{
code: 'fr',
short_name: 'FR',
flag: 'fr.svg',
style: 'width:32px;height:22px;',
},
{
code: 'fr_ch',
short_name: 'FR',
flag: 'ch.svg',
style: 'width:22px;height:22px;object-fit: cover;',
},
];
const options = computed(() =>
langs.map((lang) => ({
label: t(`language.${lang.code}`),
value: lang.code,
flagSrc: flagModules[`../assets/flags/${lang.flag}`] ?? null,
flagStyle: lang.style,
short_name: lang.short_name,
})),
);
const isAdmin = computed(() => currentUser.value?.roles.includes('admin') ?? false);
const userInitials = computed(() => {
const source = currentUser.value?.name?.trim() || currentUser.value?.email?.trim() || '?';
const parts = source.split(/\s+/).filter(Boolean);
const first = parts[0] ?? '';
const second = parts[1] ?? '';
return parts.length > 1
? `${first.charAt(0)}${second.charAt(0)}`.toUpperCase()
: first.slice(0, 2).toUpperCase();
});
onMounted(async () => {
await loadCurrentUser();
});
watch(
() => route.fullPath,
async () => {
await loadCurrentUser();
},
);
async function loadCurrentUser(): Promise<void> {
if (typeof window === 'undefined' || !window.localStorage.getItem('Auth-Token')) {
currentUser.value = null;
return;
}
const response = await me();
currentUser.value = response.error ? null : response.data;
}
async function logout(): Promise<void> {
if (typeof window !== 'undefined') {
window.localStorage.removeItem('Auth-Token');
}
currentUser.value = null;
await router.push('/');
}
</script>
<style scoped>
.lang-fallback {
display: inline-flex;
align-items: center;
justify-content: center;
min-width: 32px;
height: 22px;
padding: 0 4px;
border: 1px solid currentColor;
border-radius: 4px;
font-size: 10px;
line-height: 1;
font-weight: 700;
}
.border {
border: 1px solid #fff;
border-radius: 4px;
}
.q-select {
i.q-icon {
color: white !important;
}
}
.user-avatar {
background: linear-gradient(135deg, #0d47a1, #26a69a);
color: white;
font-size: 0.78rem;
font-weight: 700;
}
.brand-logo-tb{
height: 42px;
width: auto
}
</style>