- add AvatarController.ts
Some checks failed
CI Pipeline / japa-tests (push) Failing after 52s

- adapted menu.ts, NavBar.vue, NavBarItem.vue for highlighting active nav item
- NavBarItemLabel.vue for app menu highlighting
- adapted routes.ts
- adapted app.edge for new favicon
- adapted LayoutAuthenticated.vue (:showAsideMenu="false") for showing AsideMenu optional
- new material icons: BriefcaseCheck.vue, SwapHorizontal.vue, AccountGroup.vue, Lock.vue
- started with FirstRunWizard
This commit is contained in:
Kaimbacher 2023-12-15 17:17:33 +01:00
parent ae0c471e93
commit cefd9081ae
31 changed files with 763 additions and 126 deletions

View File

@ -0,0 +1,65 @@
import type { HttpContextContract } from '@ioc:Adonis/Core/HttpContext';
import { StatusCodes } from 'http-status-codes';
// import * as fs from 'fs';
// import * as path from 'path';
const prefixes = ['von', 'van'];
// node ace make:controller Author
export default class AvatarController {
public async generateAvatar({ request, response }: HttpContextContract) {
try {
const { name, background, textColor, size } = request.only(['name', 'background', 'textColor', 'size']);
// Generate initials
// const initials = name
// .split(' ')
// .map((part) => part.charAt(0).toUpperCase())
// .join('');
const initials = this.getInitials(name);
// Define SVG content with dynamic values for initials, background color, text color, and size
const svgContent = `
<svg width="${size || 50}" height="${size || 50}" xmlns="http://www.w3.org/2000/svg">
<rect width="100%" height="100%" fill="#${background || '7F9CF5'}"/>
<text x="50%" y="50%" dominant-baseline="middle" text-anchor="middle" font-weight="bold" font-family="Arial, sans-serif" font-size="${
(size / 100) * 40 || 25
}" fill="#${textColor || 'ffffff'}">${initials}</text>
</svg>
`;
// Set response headers for SVG content
response.header('Content-type', 'image/svg+xml');
response.header('Cache-Control', 'no-cache');
response.header('Pragma', 'no-cache');
response.header('Expires', '0');
return response.send(svgContent);
} catch (error) {
return response.status(StatusCodes.OK).json({ error: error.message });
}
}
private getInitials(name) {
const parts = name.split(' ');
let initials = '';
if (parts.length >= 2) {
const firstName = parts[0];
const lastName = parts[parts.length - 1];
const firstInitial = firstName.charAt(0).toUpperCase();
const lastInitial = lastName.charAt(0).toUpperCase();
if (prefixes.includes(lastName.toLowerCase()) && lastName === lastName.toUpperCase()) {
initials = firstInitial + lastName.charAt(1).toUpperCase();
} else {
initials = firstInitial + lastInitial;
}
} else if (parts.length === 1) {
initials = parts[0].substring(0, 2).toUpperCase();
}
return initials;
}
}

BIN
public/favicon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

View File

@ -0,0 +1,96 @@
<!--
- @copyright Copyright (c) 2023 Marco Ambrosini <marcoambrosini@proton.me>
-
- @author Simon Lindner <szaimen@e.mail.de>
- @author Marco Ambrosini <marcoambrosini@proton.me>
-
- @license GNU AGPL version 3 or any later version
-
- This program is free software: you can redistribute it and/or modify
- it under the terms of the GNU Affero General Public License as
- published by the Free Software Foundation, either version 3 of the
- License, or (at your option) any later version.
-
- This program is distributed in the hope that it will be useful,
- but WITHOUT ANY WARRANTY; without even the implied warranty of
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- GNU Affero General Public License for more details.
-
- You should have received a copy of the GNU Affero General Public License
- along with this program. If not, see <http://www.gnu.org/licenses/>.
-
-->
<template>
<element :is="isLink ? 'a' : 'div'" :href="href || undefined" class="card" :class="{ 'card--link': isLink }"
:target="!isLink ? undefined : '_blank'" :rel="!isLink ? undefined : 'noreferrer'">
<div v-if="!isLink" class="card__icon">
<slot />
</div>
<div class="card__text">
<h3 class="card__heading">
{{ title }}
</h3>
<p>{{ subtitle }}</p>
</div>
</element>
</template>
<script>
export default {
name: 'Card',
props: {
title: {
type: String,
required: true,
},
href: {
type: String,
default: '',
},
subtitle: {
type: String,
required: true,
},
},
computed: {
isLink() {
return this.href !== ''
},
},
}
</script>
<style lang="css" scoped>
.card {
display: flex;
max-width: 250px;
box-sizing: border-box;
height: fit-content;
}
.card__icon {
display: flex;
flex: 0 0 44px;
align-items: center;
}
.card__heading {
font-weight: bold;
margin: 0;
}
.card--link {
box-shadow: 0px 0px 10px 0px var(--color-box-shadow);
border-radius: var(--border-radius-large);
padding: calc(var(--default-grid-baseline) * 4);
}
.card--link:focus-visible {
outline: 2px solid var(--color-main-text);
box-shadow: 0 0 0 4px var(--color-main-background);
}
</style>

View File

@ -0,0 +1,56 @@
<template>
<div class="page__wrapper">
<div class="page__scroller first-page">
<h2 class="page__heading">
{{ t('firstrunwizard', 'A collaboration platform that puts you in control') }}
</h2>
<div class="page__content">
<Card :title="t('firstrunwizard', 'Privacy')"
:subtitle="t('firstrunwizard', 'Host your data and files where you decide.')">
<Lock :size="20" />
</Card>
<Card :title="t('firstrunwizard', 'Productivity')"
:subtitle="t('firstrunwizard', 'Collaborate and communicate across any platform.')">
<BriefcaseCheck :size="20" />
</Card>
<Card :title="t('firstrunwizard', 'Interoperability')"
:subtitle="t('firstrunwizard', 'Import and export anything you want with open standards.')">
<SwapHorizontal :size="20" />
</Card>
<Card :title="t('firstrunwizard', 'Community')"
:subtitle="t('firstrunwizard', 'Enjoy constant improvements from a thriving open-source community.')">
<AccountGroup :size="20" />
</Card>
</div>
</div>
</div>
</template>
<script>
import Card from './Card.vue'
import Lock from 'vue-material-design-icons/Lock.vue'
import BriefcaseCheck from 'vue-material-design-icons/BriefcaseCheck.vue'
import SwapHorizontal from 'vue-material-design-icons/SwapHorizontal.vue'
import AccountGroup from 'vue-material-design-icons/AccountGroup.vue'
export default {
name: 'Page1',
components: {
Card,
Lock,
BriefcaseCheck,
SwapHorizontal,
AccountGroup,
},
}
</script>
<style lang="css" scoped>
/* @import "pageStyles"; */
.first-page {
margin-top: 100px;
}
</style>

View File

@ -0,0 +1,31 @@
.page {
&__wrapper {
display: flex;
flex-direction: column;
justify-content: space-between;
min-height: min(520px, 50vh);
}
&__scroller {
overflow-y: scroll;
margin-top: calc(var(--default-grid-baseline) * 8);
}
&__heading {
text-align: center;
}
&__subtitle {
max-width: 450px;
margin: auto;
text-align: center;
}
&__content {
display: flex;
flex-wrap: wrap;
gap: calc(var(--default-grid-baseline) * 6);
justify-content: center;
margin: calc(var(--default-grid-baseline) * 10) 0;
}
}

View File

@ -0,0 +1,31 @@
<template>
<span v-bind="$attrs" :aria-hidden="!title" :aria-label="title" class="material-design-icon account-group-icon"
role="img" @click="$emit('click', $event)">
<svg :fill="fillColor" class="material-design-icon__svg" :width="size" :height="size" viewBox="0 0 24 24">
<path
d="M12,5.5A3.5,3.5 0 0,1 15.5,9A3.5,3.5 0 0,1 12,12.5A3.5,3.5 0 0,1 8.5,9A3.5,3.5 0 0,1 12,5.5M5,8C5.56,8 6.08,8.15 6.53,8.42C6.38,9.85 6.8,11.27 7.66,12.38C7.16,13.34 6.16,14 5,14A3,3 0 0,1 2,11A3,3 0 0,1 5,8M19,8A3,3 0 0,1 22,11A3,3 0 0,1 19,14C17.84,14 16.84,13.34 16.34,12.38C17.2,11.27 17.62,9.85 17.47,8.42C17.92,8.15 18.44,8 19,8M5.5,18.25C5.5,16.18 8.41,14.5 12,14.5C15.59,14.5 18.5,16.18 18.5,18.25V20H5.5V18.25M0,20V18.5C0,17.11 1.89,15.94 4.45,15.6C3.86,16.28 3.5,17.22 3.5,18.25V20H0M24,20H20.5V18.25C20.5,17.22 20.14,16.28 19.55,15.6C22.11,15.94 24,17.11 24,18.5V20Z">
<title v-if="title">{{ title }}</title>
</path>
</svg>
</span>
</template>
<script>
export default {
name: "AccountGroupIcon",
emits: ['click'],
props: {
title: {
type: String,
},
fillColor: {
type: String,
default: "currentColor"
},
size: {
type: Number,
default: 24
}
}
}
</script>

View File

@ -0,0 +1,31 @@
<template>
<span v-bind="$attrs" :aria-hidden="!title" :aria-label="title" class="material-design-icon briefcase-check-icon"
role="img" @click="$emit('click', $event)">
<svg :fill="fillColor" class="material-design-icon__svg" :width="size" :height="size" viewBox="0 0 24 24">
<path
d="M10,2H14A2,2 0 0,1 16,4V6H20A2,2 0 0,1 22,8V19A2,2 0 0,1 20,21H4A2,2 0 0,1 2,19V8A2,2 0 0,1 4,6H8V4A2,2 0 0,1 10,2M14,6V4H10V6H14M10.5,17.5L17.09,10.91L15.68,9.5L10.5,14.67L8.41,12.59L7,14L10.5,17.5Z">
<title v-if="title">{{ title }}</title>
</path>
</svg>
</span>
</template>
<script>
export default {
name: "BriefcaseCheckIcon",
emits: ['click'],
props: {
title: {
type: String,
},
fillColor: {
type: String,
default: "currentColor"
},
size: {
type: Number,
default: 24
}
}
}
</script>

View File

@ -0,0 +1,31 @@
<template>
<span v-bind="$attrs" :aria-hidden="!title" :aria-label="title" class="material-design-icon lock-icon" role="img"
@click="$emit('click', $event)">
<svg :fill="fillColor" class="material-design-icon__svg" :width="size" :height="size" viewBox="0 0 24 24">
<path
d="M12,17A2,2 0 0,0 14,15C14,13.89 13.1,13 12,13A2,2 0 0,0 10,15A2,2 0 0,0 12,17M18,8A2,2 0 0,1 20,10V20A2,2 0 0,1 18,22H6A2,2 0 0,1 4,20V10C4,8.89 4.9,8 6,8H7V6A5,5 0 0,1 12,1A5,5 0 0,1 17,6V8H18M12,3A3,3 0 0,0 9,6V8H15V6A3,3 0 0,0 12,3Z">
<title v-if="title">{{ title }}</title>
</path>
</svg>
</span>
</template>
<script>
export default {
name: "LockIcon",
emits: ['click'],
props: {
title: {
type: String,
},
fillColor: {
type: String,
default: "currentColor"
},
size: {
type: Number,
default: 24
}
}
}
</script>

View File

@ -0,0 +1,30 @@
<template>
<span v-bind="$attrs" :aria-hidden="!title" :aria-label="title" class="material-design-icon swap-horizontal-icon"
role="img" @click="$emit('click', $event)">
<svg :fill="fillColor" class="material-design-icon__svg" :width="size" :height="size" viewBox="0 0 24 24">
<path d="M21,9L17,5V8H10V10H17V13M7,11L3,15L7,19V16H14V14H7V11Z">
<title v-if="title">{{ title }}</title>
</path>
</svg>
</span>
</template>
<script>
export default {
name: "SwapHorizontalIcon",
emits: ['click'],
props: {
title: {
type: String,
},
fillColor: {
type: String,
default: "currentColor"
},
size: {
type: Number,
default: 24
}
}
}
</script>

View File

@ -19,12 +19,15 @@ import {
mdiCloudDownloadOutline,
mdiCloud,
mdiCrop,
mdiAccount,
mdiCogOutline,
mdiEmail,
mdiAccountCog,
mdiFormatListGroup ,
mdiFormatListNumbered,
// mdiEmail,
mdiLogout,
mdiGithub,
mdiThemeLightDark,
mdiViewDashboard,
mdiMapSearch
} from '@mdi/js';
import NavBarItem from '@/Components/NavBarItem.vue';
import NavBarItemLabel from '@/Components/NavBarItemLabel.vue';
@ -32,20 +35,25 @@ import NavBarMenu from '@/Components/NavBarMenu.vue';
import BaseDivider from '@/Components/BaseDivider.vue';
import UserAvatarCurrentUser from '@/Components/UserAvatarCurrentUser.vue';
import BaseIcon from '@/Components/BaseIcon.vue';
import NavBarSearch from '@/Components/NavBarSearch.vue';
// import NavBarSearch from '@/Components/NavBarSearch.vue';
import { stardust } from '@eidellev/adonis-stardust/client';
import type { User } from '@/Dataset';
// import FirstrunWizard from '@/Components/FirstrunWizard/FirstrunWizard.vue'
import Lock from 'vue-material-design-icons/Lock.vue'
// import BriefcaseCheck from 'vue-material-design-icons/BriefcaseCheck.vue'
// import SwapHorizontal from 'vue-material-design-icons/SwapHorizontal.vue'
// import AccountGroup from 'vue-material-design-icons/AccountGroup.vue'
// const mainStore = MainService();
// const userName = computed(() =>mainStore.userName);
const styleService = StyleService();
// const props = defineProps({
// user: {
// type: Object,
// default: () => ({}),
// }
// });
const props = defineProps({
showBurger: {
type: Boolean,
default: true // Set default value to true
}
});
// const userName = computed(() => usePage().props.user.login)
const user: ComputedRef<User> = computed(() => {
@ -80,32 +88,41 @@ const logout = async () => {
// router.post(route('logout'));
await router.post(stardust.route('logout'));
};
</script>
<template>
<nav class="top-0 left-0 right-0 fixed bg-gray-50 h-14 z-40 w-screen transition-position xl:pl-60 lg:w-auto dark:bg-slate-800">
<nav class="text-base top-0 left-0 right-0 fixed bg-gray-50 h-14 z-40 w-screen transition-position lg:w-auto dark:bg-slate-800"
:class="{ 'xl:pl-60': props.showBurger == true }">
<div class="flex lg:items-stretch" :class="containerMaxW">
<div class="flex-1 items-stretch flex h-14">
<NavBarItem type="flex lg:hidden" @click.prevent="layoutStore.asideMobileToggle()">
<NavBarItem type="flex lg:hidden" @click.prevent="layoutStore.asideMobileToggle()" v-if="props.showBurger">
<BaseIcon :path="layoutStore.isAsideMobileExpanded ? mdiBackburger : mdiForwardburger" size="24" />
</NavBarItem>
<NavBarItem type="hidden lg:flex xl:hidden" @click.prevent="menuOpenLg">
<NavBarItem type="hidden lg:flex xl:hidden" @click.prevent="menuOpenLg" v-if="props.showBurger">
<BaseIcon :path="mdiMenu" size="24" />
</NavBarItem>
<NavBarItem>
<NavBarSearch />
<NavBarItem route-name="apps.dashboard">
<NavBarItemLabel :icon="mdiViewDashboard" label="Dashboard" size="22" is-hover-label-only route-name="apps.dashboard" />
</NavBarItem>
<NavBarItem route-name="apps.map">
<NavBarItemLabel :icon="mdiMapSearch" label="Map" size="22" is-hover-label-only route-name="apps.map" />
</NavBarItem>
<!-- <NavBarItem>
<NavBarSearch />
</NavBarItem> -->
</div>
<div class="flex-none items-stretch flex h-14 lg:hidden">
<NavBarItem @click.prevent="menuNavBarToggle">
<BaseIcon :path="isMenuNavBarActive ? mdiClose : mdiDotsVertical" size="24" />
</NavBarItem>
</div>
<div
class="absolute w-screen top-14 left-0 bg-gray-50 shadow lg:w-auto lg:items-stretch lg:flex lg:grow lg:static lg:border-b-0 lg:overflow-visible lg:shadow-none dark:bg-slate-800"
:class="[isMenuNavBarActive ? 'block' : 'hidden']"
>
<div class="max-h-screen-menu overflow-y-auto lg:overflow-visible lg:flex lg:items-stretch lg:justify-end lg:ml-auto">
<div class="absolute w-screen top-14 left-0 bg-gray-50 shadow lg:w-auto lg:items-stretch lg:flex lg:grow lg:static lg:border-b-0 lg:overflow-visible lg:shadow-none dark:bg-slate-800"
:class="[isMenuNavBarActive ? 'block' : 'hidden']">
<div
class="max-h-screen-menu overflow-y-auto lg:overflow-visible lg:flex lg:items-stretch lg:justify-end lg:ml-auto">
<!-- help menu -->
<NavBarMenu>
<NavBarItemLabel :icon="mdiMenu" label="Help menu" />
<template #dropdown>
@ -125,23 +142,29 @@ const logout = async () => {
</template>
</NavBarMenu>
<!-- personal menu -->
<NavBarMenu>
<NavBarItemLabel v-bind:label="user.login">
<NavBarItemLabel v-bind:label="`hello ${user.login}`">
<UserAvatarCurrentUser class="w-6 h-6 mr-3 inline-flex" />
</NavBarItemLabel>
<template #dropdown>
<!-- <NavBarItem> -->
<!-- <NavBarItem route-name="admin.account.info"> -->
<NavBarItem :route-name="'settings.user'">
<NavBarItemLabel :icon="mdiAccount" label="My Profile" />
<NavBarItemLabel :icon="mdiAccountCog" label="User Settings" />
</NavBarItem>
<NavBarItem v-if="userHasRoles(['moderator', 'administrator'])" :route-name="'settings'">
<NavBarItemLabel :icon="mdiCogOutline" label="Settings" />
</NavBarItem>
<NavBarItem>
<NavBarItem v-if="userHasRoles(['administrator'])" :route-name="'settings.overview'">
<NavBarItemLabel :icon="mdiFormatListGroup" label="Administration" />
</NavBarItem>
<NavBarItem v-if="userHasRoles(['submitter'])" :route-name="'dataset.list'">
<NavBarItemLabel :icon="mdiFormatListNumbered" label="Submitter Setup" />
</NavBarItem>
<NavBarItem v-if="userHasRoles(['editor'])" :route-name="'editor.dataset.list'">
<NavBarItemLabel :icon="mdiFormatListNumbered" label="Editor Setup" />
</NavBarItem>
<!-- <NavBarItem>
<NavBarItemLabel :icon="mdiEmail" label="Messages" />
</NavBarItem>
</NavBarItem> -->
<BaseDivider nav-bar />
<NavBarItem @click="logout">
<NavBarItemLabel :icon="mdiLogout" label="Log Out" />

View File

@ -1,4 +1,4 @@
<script setup>
<script lang="ts" setup>
import { StyleService } from '@/Stores/style';
// import { Link } from '@inertiajs/vue3'
import { Link } from '@inertiajs/vue3';
@ -28,6 +28,10 @@ const props = defineProps({
isDesktopIconOnly: Boolean,
dropdown: Boolean,
active: Boolean,
underline: {
type: Boolean,
default: false,
},
});
const is = computed(() => {
@ -44,20 +48,50 @@ const is = computed(() => {
const styleStore = StyleService();
const activeColor = props.activeColor ?? `${styleStore.navBarItemLabelActiveColorStyle} dark:text-slate-400`;
// const activeColor = props.activeColor ?? `${styleStore.navBarItemLabelActiveColorStyle} dark:text-slate-400`;
const activeClass = computed(
// () => props.routeName && route().current(props.routeName) == true ? props.activeColor : null
() => (props.routeName && stardust.isCurrent(props.routeName) ? props.activeColor : null),
);
// const active = computed(() => {
// if (props.routeName && stardust.isCurrent(props.routeName)) {
// return true;
// }
// else {
// return false;
// }
// });
const activeClass = computed(() => {
if (props.routeName && stardust.isCurrent(props.routeName)) {
// console.log(props.item.route);
const activeCls = [
styleStore.navBarItemLabelActiveColorStyle,
'dark:text-slate-400'
];
if (props.underline) {
activeCls.push('app-menu-entry__active');
}
// return `${styleStore.navBarItemLabelActiveColorStyle} dark:text-slate-400 app-menu-entry__active`;
return activeCls;
} else {
return null;
}
});
// const activeClass = computed(
// // () => props.routeName && route().current(props.routeName) == true ? props.activeColor : null
// () => (props.routeName && stardust.isCurrent(props.routeName) ? props.activeColor : null),
// );
// const itemRoute = computed(() => (props.routeName ? stardust.route(props.routeName): ''));
const componentClass = computed(() => {
const base = [
props.type,
props.active
? activeColor
: `${styleStore.navBarItemLabelStyle} dark:text-white dark:hover:text-slate-400 ${styleStore.navBarItemLabelHoverStyle}`,
`${styleStore.navBarItemLabelStyle} dark:text-white dark:hover:text-slate-400 ${styleStore.navBarItemLabelHoverStyle}`,
// props.active
// ? activeColor
// : `${styleStore.navBarItemLabelStyle} dark:text-white dark:hover:text-slate-400 ${styleStore.navBarItemLabelHoverStyle}`,
];
if (props.type === 'block') {
@ -79,12 +113,31 @@ const componentClass = computed(() => {
</script>
<template>
<component
:is="is"
class="items-center grow-0 shrink-0 relative cursor-pointer"
:class="[componentClass, activeClass]"
:href="routeName ? stardust.route(props.routeName, [props.param]) : href"
>
<component :is="is" class="items-center grow-0 shrink-0 relative cursor-pointer" :class="[componentClass, activeClass]"
:href="routeName ? stardust.route(props.routeName, [props.param]) : href">
<slot />
</component>
</template>
<style lang="css" scoped>
.app-menu-entry__active {
opacity: 1;
}
.app-menu-entry__active::before {
content: "___";
position: absolute;
pointer-events: none;
/* border-bottom-color: var(--color-main-background); */
/* transform: translateX(-50%); */
width: 100%;
/* height: 5px; */
border-radius: 3px;
background-color: var(--color-primary-element-text);
/* left: 50%; */
bottom: 6px;
display: block;
transition: all 0.1s ease-in-out;
opacity: 1;
}
</style>

View File

@ -1,7 +1,9 @@
<script setup>
<script lang="ts" setup>
import BaseIcon from '@/Components/BaseIcon.vue';
import { computed } from 'vue';
import { stardust } from '@eidellev/adonis-stardust/client';
defineProps({
const props = defineProps({
icon: {
type: String,
default: null,
@ -10,12 +12,90 @@ defineProps({
type: String,
required: true,
},
size: {
type: [String, Number],
default: 16,
},
isDesktopIconOnly: Boolean,
isHoverLabelOnly: Boolean,
routeName: {
type: String,
default: null,
},
});
const active = computed(() => {
if (props.routeName && stardust.isCurrent(props.routeName)) {
return true;
}
else {
return false;
}
});
const componentClass = computed(() => {
const base = [
// props.type,
'flex',
];
if (props.isHoverLabelOnly ) {
base.push(' flex-col items-center');
}
return base;
});
const labelClass = computed(() => {
const base = [
// props.type,
'transition',
];
if (props.isDesktopIconOnly && props.icon) {
base.push('lg:hidden');
}
if (props.isHoverLabelOnly && active.value == false) {
base.push('hidden');
} else if(props.isHoverLabelOnly && active.value == true) {
base.push('display-block');
}
return base;
});
</script>
<template>
<slot />
<BaseIcon v-if="icon" :path="icon" class="transition-colors" />
<span class="px-2 transition-colors" :class="{ 'lg:hidden': isDesktopIconOnly && icon }">{{ label }}</span>
<div :class="componentClass">
<slot />
<BaseIcon v-if="icon" :path="props.icon" class="transition-colors" :size="props.size" />
<!-- <span class="px-2 transition-colors" :class="{ 'lg:hidden': isDesktopIconOnly && props.icon }">{{ props.label }}</span> -->
<span class="px-2 transition-colors" :class="labelClass">{{ props.label }}</span>
</div>
</template>
<!-- <style lang="css" scoped>
.app-menu-entry__active {
opacity: 1;
}
.app-menu-entry__active::before {
/* content: "___"; */
position: absolute;
pointer-events: none;
/* border-bottom-color: var(--color-main-background); */
/* transform: translateX(-50%); */
width: 100%;
/* height: 5px; */
border-radius: 3px;
border-bottom: 2px;
/* background-color: var(--color-primary-element-text); */
/* left: 50%; */
bottom: 6px;
display: block;
transition: all 0.1s ease-in-out;
opacity: 1;
}
</style> -->

View File

@ -19,19 +19,97 @@ const props = defineProps({
const avatar = computed(
// () => props.avatar ?? `https://avatars.dicebear.com/api/${props.api}/${props.username?.replace(/[^a-z0-9]+/i, '-')}.svg`
() => props.avatar ?? `https://avatars.dicebear.com/api/initials/${props.username}.svg`,
// () => props.avatar ?? `https://avatars.dicebear.com/api/initials/${props.username}.svg`,
// () => {
() => {
const initials = props.username
.split(' ')
.map((part) => part.charAt(0).toUpperCase())
.join('');
// return props.avatar ?? `https://www.gravatar.com/avatar/${props.username}?s=50`;
// }
return props.avatar ?? generateAvatarUrl(props.username);
},
);
const username = computed(() => props.username);
const darkenColor = (color) => {
// Convert hex to RGB
const r = parseInt(color.slice(0, 2), 16);
const g = parseInt(color.slice(2, 4), 16);
const b = parseInt(color.slice(4, 6), 16);
// Calculate darker color by reducing 20% of each RGB component
const darkerR = Math.round(r * 0.6);
const darkerG = Math.round(g * 0.6);
const darkerB = Math.round(b * 0.6);
// Convert back to hex
const darkerColor = ((darkerR << 16) + (darkerG << 8) + darkerB).toString(16);
return darkerColor.padStart(6, '0'); // Ensure it's 6 digits
};
const getRandomColor = () => {
return Math.floor(Math.random() * 16777215).toString(16);
};
const adjustOpacity = (hexColor, opacity) => {
// Remove # if present
hexColor = hexColor.replace('#', '');
// Convert hex to RGB
// const r = parseInt(hexColor.slice(0, 2), 16);
// const g = parseInt(hexColor.slice(2, 4), 16);
// const b = parseInt(hexColor.slice(4, 6), 16);
// const r = parseInt(hexColor.slice(1, 3), 16);
// const g = parseInt(hexColor.slice(3, 5), 16);
// const b = parseInt(hexColor.slice(5, 7), 16);
const [r, g, b] = hexColor.match(/\w\w/g).map(x => parseInt(x, 16));
return `rgba(${r},${g},${b},${opacity})`;
};
const lightenColor = (hexColor, percent) => {
let r = parseInt(hexColor.substring(0, 2), 16);
let g = parseInt(hexColor.substring(2, 4), 16);
let b = parseInt(hexColor.substring(4, 6), 16);
r = Math.floor(r * (100 + percent) / 100);
g = Math.floor(g * (100 + percent) / 100);
b = Math.floor(b * (100 + percent) / 100);
r = (r < 255) ? r : 255;
g = (g < 255) ? g : 255;
b = (b < 255) ? b : 255;
const lighterHex = ((r << 16) | (g << 8) | b).toString(16);
return lighterHex.padStart(6, '0');
};
// backgroundColor = '7F9CF5',
const generateAvatarUrl = (name) => {
const initials = name
.split(' ')
.map((part) => part.charAt(0).toUpperCase())
.join('');
const originalColor = getRandomColor();
const backgroundColor = lightenColor(originalColor, 60); // Lighten by 20%
const textColor = darkenColor(originalColor);
// const avatarUrl = `https://ui-avatars.com/api/?name=${initials}&size=50&background=${backgroundColor}&color=${textColor}`;
const avatarUrl = `/api/avatar?name=${name}&size=50&background=${backgroundColor}&textColor=${textColor}`;
return avatarUrl;
};
</script>
<template>
<div>
<img :src="avatar" :alt="username" class="rounded-full block h-auto w-full max-w-full bg-gray-100 dark:bg-slate-800" />
<img :src="avatar" :alt="username"
class="rounded-full block h-auto w-full max-w-full bg-gray-100 dark:bg-slate-800" />
</div>
</template>

View File

@ -10,27 +10,33 @@ const styleService = StyleService();
const layoutService = LayoutService();
// defineProps({
// user: {
// type: Object,
// default: () => ({}),
// }
// });
const props = defineProps({
showAsideMenu: {
type: Boolean,
default: true // Set default value to true
}
// user: {
// type: Object,
// default: () => ({}),
// }
});
</script>
<template>
<div
:class="{
'dark': styleService.darkMode,
'overflow-hidden lg:overflow-visible': layoutService.isAsideMobileExpanded,
}"
>
<div
:class="{ 'ml-60 lg:ml-0': layoutService.isAsideMobileExpanded }"
class="pt-14 xl:pl-60 min-h-screen w-screen transition-position lg:w-auto bg-gray-50 dark:bg-slate-800 dark:text-slate-100"
>
<NavBar :class="{ 'ml-60 lg:ml-0': layoutService.isAsideMobileExpanded }" />
<AsideMenu />
<div :class="{
'dark': styleService.darkMode,
'overflow-hidden lg:overflow-visible': layoutService.isAsideMobileExpanded,
}">
<div :class="{
'ml-60 lg:ml-0': layoutService.isAsideMobileExpanded,
'xl:pl-60': props.showAsideMenu==true }"
class="pt-14 min-h-screen w-screen transition-position lg:w-auto bg-gray-50 dark:bg-slate-800 dark:text-slate-100">
<NavBar :class="{ 'ml-60 lg:ml-0': layoutService.isAsideMobileExpanded }" :showBurger="props.showAsideMenu" />
<!-- Conditionally render AsideMenu based on showAsideMenu prop -->
<template v-if="showAsideMenu">
<AsideMenu />
</template>
<!-- slot for main content -->
<slot></slot>
<FooterBar />
</div>

View File

@ -28,7 +28,7 @@ const form = useForm({
});
const submit = async () => {
await form.post(stardust.route('role.store'), form);
await form.post(stardust.route('settings.role.store'), form);
};
</script>
@ -38,7 +38,7 @@ const submit = async () => {
<SectionMain>
<SectionTitleLineWithButton :icon="mdiAccountKey" title="Add role" main>
<BaseButton
:route-name="stardust.route('role.index')"
:route-name="stardust.route('settings.role.index')"
:icon="mdiArrowLeftBoldOutline"
label="Back"
color="white"

View File

@ -37,7 +37,7 @@ const form = useForm({
const submit = async () => {
// await Inertia.post(stardust.route('user.store'), form);
await form.put(stardust.route('role.update', [props.role.id]), form);
await form.put(stardust.route('settings.role.update', [props.role.id]), form);
};
</script>
@ -47,7 +47,7 @@ const submit = async () => {
<SectionMain>
<SectionTitleLineWithButton :icon="mdiAccountKey" title="Update role" main>
<BaseButton
:route-name="stardust.route('role.index')"
:route-name="stardust.route('settings.role.index')"
:icon="mdiArrowLeftBoldOutline"
label="Back"
color="white"

View File

@ -47,7 +47,7 @@ const formDelete = useForm({});
// function destroy(id) {
// const destroy = async (id) => {
// if (confirm('Are you sure you want to delete?')) {
// await formDelete.delete(stardust.route('role.destroy', [id]));
// await formDelete.delete(stardust.route('settings.role.destroy', [id]));
// }
// };
@ -59,7 +59,7 @@ const destroy = (id, e) => {
const onConfirm = async (id) => {
// let id = 6;
await formDelete.delete(stardust.route('role.destroy', [id]));
await formDelete.delete(stardust.route('settings.role.destroy', [id]));
deleteId.value = null;
};
@ -90,7 +90,7 @@ const onCancel = (id) => {
<SectionTitleLineWithButton :icon="mdiAccountKey" title="Roles" main>
<BaseButton
v-if="can.create"
:route-name="stardust.route('role.create')"
:route-name="stardust.route('settings.role.create')"
:icon="mdiPlus"
label="Add"
color="info"
@ -139,7 +139,7 @@ const onCancel = (id) => {
<tr v-for="role in roles" :key="role.id">
<td data-label="Name">
<Link
:href="stardust.route('role.show', [role.id])"
:href="stardust.route('settings.role.show', [role.id])"
class="no-underline hover:underline text-cyan-600 dark:text-cyan-400"
>
{{ role.name }}
@ -152,7 +152,7 @@ const onCancel = (id) => {
<BaseButtons type="justify-start lg:justify-end" no-wrap>
<BaseButton
v-if="can.edit"
:route-name="stardust.route('role.edit', [role.id])"
:route-name="stardust.route('settings.role.edit', [role.id])"
color="info"
:icon="mdiSquareEditOutline"
small

View File

@ -30,7 +30,7 @@ const props = defineProps({
<SectionMain>
<SectionTitleLineWithButton :icon="mdiAccountKey" title="View role" main>
<BaseButton
:route-name="stardust.route('role.index')"
:route-name="stardust.route('settings.role.index')"
:icon="mdiArrowLeftBoldOutline"
label="Back"
color="white"

View File

@ -34,7 +34,7 @@ const form = useForm({
});
const submit = async () => {
await router.post(stardust.route('user.store'), form);
await router.post(stardust.route('settings.user.store'), form);
};
</script>
@ -44,7 +44,7 @@ const submit = async () => {
<SectionMain>
<SectionTitleLineWithButton :icon="mdiAccountKey" title="Add user" main>
<BaseButton
:route-name="stardust.route('user.index')"
:route-name="stardust.route('settings.user.index')"
:icon="mdiArrowLeftBoldOutline"
label="Back"
color="modern"
@ -52,7 +52,7 @@ const submit = async () => {
small
/>
</SectionTitleLineWithButton>
<!-- @submit.prevent="form.post(stardust.route('user.store'))" -->
<!-- @submit.prevent="form.post(stardust.route('settings.user.store'))" -->
<CardBox form @submit.prevent="submit()">
<FormField label="Login" :class="{ 'text-red-400': errors.login }">
<FormControl v-model="form.login" type="text" placeholder="Enter Login" :errors="errors.login">

View File

@ -44,7 +44,7 @@ const form = useForm({
const submit = async () => {
// await Inertia.post(stardust.route('user.store'), form);
await router.put(stardust.route('user.update', [props.user.id]), form);
await router.put(stardust.route('settings.user.update', [props.user.id]), form);
};
</script>
@ -54,7 +54,7 @@ const submit = async () => {
<SectionMain>
<SectionTitleLineWithButton :icon="mdiAccountKey" title="Update user" main>
<BaseButton
:route-name="stardust.route('user.index')"
:route-name="stardust.route('settings.user.index')"
:icon="mdiArrowLeftBoldOutline"
label="Back"
color="white"

View File

@ -66,7 +66,7 @@ const destroy = async (id) => {
<SectionTitleLineWithButton :icon="mdiAccountKey" title="Tethys Users" main>
<BaseButton
v-if="can.create"
:route-name="stardust.route('user.create')"
:route-name="stardust.route('settings.user.create')"
:icon="mdiPlus"
label="Add"
color="modern"
@ -80,7 +80,7 @@ const destroy = async (id) => {
</NotificationBar>
<!-- <NotificationBar color="success" :icon="mdiAlertBoxOutline">{{ users.meta }}</NotificationBar> -->
<CardBox class="mb-6" has-table>
<form @submit.prevent="form.get(stardust.route('user.index'))">
<form @submit.prevent="form.get(stardust.route('settings.user.index'))">
<div class="py-2 flex">
<div class="flex pl-4">
<input
@ -113,7 +113,7 @@ const destroy = async (id) => {
<tr v-for="user in users.data" :key="user.id">
<td data-label="Login">
<Link
v-bind:href="stardust.route('user.show', [user.id])"
v-bind:href="stardust.route('settings.user.show', [user.id])"
class="no-underline hover:underline text-cyan-600 dark:text-cyan-400"
>
{{ user.login }}
@ -127,7 +127,7 @@ const destroy = async (id) => {
<BaseButtons type="justify-start lg:justify-end" no-wrap>
<BaseButton
v-if="can.edit"
:route-name="stardust.route('user.edit', [user.id])"
:route-name="stardust.route('settings.user.edit', [user.id])"
color="info"
:icon="mdiSquareEditOutline"
small

View File

@ -30,7 +30,7 @@ defineProps({
<SectionMain>
<SectionTitleLineWithButton :icon="mdiAccountKey" title="View user" main>
<BaseButton
:route-name="stardust.route('user.index')"
:route-name="stardust.route('settings.user.index')"
:icon="mdiArrowLeftBoldOutline"
label="Back"
color="white"

View File

@ -57,7 +57,7 @@ const datasets = computed(() => mainService.datasets);
</script>
<template>
<LayoutAuthenticated>
<LayoutAuthenticated :showAsideMenu="false">
<Head title="Dashboard" />
<!-- <section class="p-6" v-bind:class="containerMaxW"> -->

View File

@ -76,7 +76,7 @@ const getRowClass = (dataset) => {
<!-- table -->
<CardBox class="mb-6" has-table>
<div v-if="props.datasets && props.datasets.length > 0">
<div v-if="props.datasets.data.length > 0">
<table class="pure-table">
<thead>
<tr>

View File

@ -2,7 +2,7 @@
import { Head } from '@inertiajs/vue3';
import { ref, Ref } from 'vue';
import { mdiChartTimelineVariant } from '@mdi/js';
import LayoutGuest from '@/Layouts/LayoutGuest.vue';
import LayoutAuthenticated from '@/Layouts/LayoutAuthenticated.vue';
import SectionMain from '@/Components/SectionMain.vue';
import BaseButton from '@/Components/BaseButton.vue';
import SectionTitleLineWithButton from '@/Components/SectionTitleLineWithButton.vue';
@ -47,7 +47,7 @@ const mapOptions: MapOptions = {
</script>
<template>
<LayoutGuest>
<LayoutAuthenticated :showAsideMenu="false">
<Head title="Map" />
<!-- <section class="p-6" v-bind:class="containerMaxW"> -->
@ -99,5 +99,5 @@ const mapOptions: MapOptions = {
</div>
</SectionMain>
<!-- </section> -->
</LayoutGuest>
</LayoutAuthenticated>
</template>

View File

@ -101,7 +101,7 @@ const getRowClass = (dataset) => {
<tbody class="bg-white divide-y divide-gray-200">
<tr v-for="dataset in props.datasets.data" :key="dataset.id" :class="getRowClass(dataset)">
<td data-label="Login" class="py-4 whitespace-nowrap text-gray-700 dark:text-white">
<!-- <Link v-bind:href="stardust.route('user.show', [user.id])"
<!-- <Link v-bind:href="stardust.route('settings.user.show', [user.id])"
class="no-underline hover:underline text-cyan-600 dark:text-cyan-400">
{{ user.login }}
</Link> -->

View File

@ -1,5 +1,5 @@
import {
mdiMonitor,
// mdiMonitor,
mdiGithub,
mdiAccountEye,
mdiAccountGroup,
@ -7,13 +7,15 @@ import {
mdiPublish,
mdiAccountArrowUp,
mdiFormatListNumbered,
mdiLock
mdiLock,
mdiFormatListGroup,
mdiShieldCrownOutline,
} from '@mdi/js';
export default [
{
// route: 'dataset.create',
icon: mdiAccountArrowUp,
icon: mdiAccountEdit ,
label: 'Personal',
// roles: ['submitter'],
isOpen: true,
@ -30,11 +32,11 @@ export default [
// },
],
},
{
route: 'apps.dashboard',
icon: mdiMonitor,
label: 'Dashboard',
},
// {
// route: 'apps.dashboard',
// icon: mdiMonitor,
// label: 'Dashboard',
// },
// {
// route: 'permission.index',
// icon: mdiAccountKey,
@ -46,23 +48,33 @@ export default [
// label: 'Roles'
// },
{
route: 'user.index',
icon: mdiAccountGroup,
label: 'Users',
icon: mdiShieldCrownOutline,
label: 'Administration',
roles: ['administrator'],
isOpen: true,
permanent: true,
children: [
{
route: 'settings.overview',
icon: mdiFormatListGroup,
label: 'Overview',
roles: ['administrator'],
},
{
route: 'settings.user.index',
icon: mdiAccountGroup,
label: 'Users',
roles: ['administrator'],
},
{
route: 'settings.role.index',
icon: mdiAccountEye,
label: 'Roles',
roles: ['administrator'],
},
],
},
{
route: 'role.index',
icon: mdiAccountEye,
label: 'Roles',
roles: ['administrator'],
},
{
href: '/oai',
icon: mdiAccountEye,
label: 'OAI',
target: '_blank',
},
{
// route: 'dataset.create',
icon: mdiAccountArrowUp,
@ -112,4 +124,10 @@ export default [
label: 'Gitea',
target: '_blank',
},
{
href: '/oai',
icon: mdiAccountEye,
label: 'OAI',
target: '_blank',
},
];

View File

@ -3,7 +3,9 @@
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="icon" type="image/png" href="/favicon.ico">
{{-- <link rel="icon" type="image/png" href="/favicon.ico"> --}}
<link rel="icon" type="image/png" href="/favicon.png">
{{-- <link rel="icon" href="/apps/theming/favicon/settings?v=ad28c447"> --}}
@routes
@entryPointStyles('app')

View File

@ -54,6 +54,10 @@ Route.group(() => {
return inertia.render('Dashboard');
}).as('dashboard');
Route.get('/map', async ({ inertia }) => {
return inertia.render('Map');
}).as('map');
Route.get('/', async ({ inertia }) => {
const users = await User.query().orderBy('login');
return inertia.render('App', {
@ -85,9 +89,7 @@ Route.group(() => {
.as('apps')
.middleware('auth');
Route.get('/map', async ({ inertia }) => {
return inertia.render('Map');
}).as('map');
// Route.on("/login").render("signin");
Route.get('/app/login', async ({ inertia }) => {
@ -104,7 +106,8 @@ Route.post('/signout', 'Auth/AuthController.logout').as('logout');
Route.group(() => {
Route.get('/settings', async ({ inertia }) => {
return inertia.render('Admin/Settings');
}).as('settings');
}).as('overview');
Route.get('/user', 'UsersController.index').as('user.index').middleware(['can:user-list']);
Route.get('/user/create', 'UsersController.create').as('user.create').middleware(['can:user-create']);
@ -137,6 +140,7 @@ Route.group(() => {
})
.namespace('App/Controllers/Http/Admin')
.prefix('admin')
.as('settings')
// .middleware(['auth', 'can:dataset-list,dataset-publish']);
.middleware(['auth', 'is:administrator,moderator']);
@ -189,7 +193,7 @@ Route.group(() => {
.middleware(['auth', 'can:dataset-delete']);
Route.get('/person', 'PersonController.index').as('person.index').middleware(['auth']);
// Route.get('/user/:id/edit', 'UsersController.edit').as('user.edit').where('id', Route.matchers.number());
// Route.get('/user/:id/edit', 'UsersController.edit').as('settings.user.edit').where('id', Route.matchers.number());
// Route.put('/user/:id/update', 'UsersController.update').as('user.update').where('id', Route.matchers.number());
// Route.delete('/user/:id', 'UsersController.destroy').as('user.destroy').where('id', Route.matchers.number());
// Route.resource('user', 'DatasetController');

View File

@ -18,6 +18,8 @@ Route.group(() => {
Route.get('/years', 'HomeController.findYears');
Route.get('/download/:id', 'FileController.findOne').as('file.findOne');
Route.get('/avatar/:name/:background?/:textColor?/:size?', 'AvatarController.generateAvatar')
});
// .middleware("auth:api");
})

Binary file not shown.

Before

Width:  |  Height:  |  Size: 119 KiB