- add validator for checking languages of translated titles and description

- add notification messages via notiwind.ts
- add Project.ts
- add new component TabelPersons.vue
- add additional routes
- npm updates
This commit is contained in:
Kaimbacher 2023-03-24 11:41:52 +01:00
parent 59a99ff3c8
commit 080c21126b
24 changed files with 979 additions and 349 deletions

View file

@ -1,37 +1,34 @@
<script>
import { h, defineComponent } from 'vue'
import { h, defineComponent } from 'vue';
export default defineComponent({
name: 'BaseLevel',
props: {
mobile: Boolean,
type: {
type: String,
default: 'justify-between'
}
},
render () {
const parentClass = [this.type, 'items-center']
name: 'BaseLevel',
props: {
mobile: Boolean,
type: {
type: String,
default: 'justify-between',
},
},
render() {
const parentClass = [this.type, 'items-center'];
const parentMobileClass = ['flex']
const parentMobileClass = ['flex'];
const parentBaseClass = ['block', 'md:flex']
const parentBaseClass = ['block', 'md:flex'];
const childBaseClass = ['flex', 'shrink-0', 'grow-0', 'items-center', 'justify-center']
const childBaseClass = ['flex', 'shrink-0', 'grow-0', 'items-center', 'justify-center'];
return h(
'div',
{ class: parentClass.concat(this.mobile ? parentMobileClass : parentBaseClass) },
this.$slots.default().map((element, index) => {
const childClass = (!this.mobile && this.$slots.default().length > index + 1)
? childBaseClass.concat(['mb-6', 'md:mb-0'])
: childBaseClass
return h(
'div',
{ class: parentClass.concat(this.mobile ? parentMobileClass : parentBaseClass) },
this.$slots.default().map((element, index) => {
const childClass =
!this.mobile && this.$slots.default().length > index + 1 ? childBaseClass.concat(['mb-6', 'md:mb-0']) : childBaseClass;
return h('div', { class: childClass }, [
element
])
})
)
}
})
return h('div', { class: childClass }, [element]);
}),
);
},
});
</script>

View file

@ -112,7 +112,7 @@ if (props.ctrlKFocus) {
<div class="relative">
<select v-if="computedType === 'select'" :id="id" v-model="computedValue" :name="name" :class="inputElClass">
<option v-if="placeholder" class="text-opacity-25" value="" disabled selected>{{ placeholder }}</option>
<option v-for="option in options" :key="option.id ?? option" :value="option.value ?? option">
<option v-for="(option, index) in options" :key="index" :value="option.value ?? index">
{{ option.label ?? option }}
</option>
</select>

View file

@ -0,0 +1,66 @@
<script setup lang="ts">
import { Notification, NotificationGroup } from '@/notiwind';
</script>
<template>
<NotificationGroup position="bottom">
<div class="z-50 fixed inset-x-0 top-0 flex items-start justify-end p-6 px-4 py-6 pointer-events-none">
<div class="w-full max-w-sm">
<Notification
v-slot="{ notifications }"
enter="transform ease-out duration-300 transition"
enter-from="translate-y-2 opacity-0 sm:translate-y-0 sm:translate-x-4"
enter-to="translate-y-0 opacity-100 sm:translate-x-0"
leave="transition ease-in duration-500"
leave-from="opacity-100"
leave-to="opacity-0"
move="transition duration-500"
move-delay="delay-300"
>
<div v-for="notification in notifications" :key="notification.id">
<div
v-if="notification.type === 'info'"
class="flex w-full max-w-sm mx-auto mt-4 overflow-hidden bg-white rounded-lg shadow-md"
>
<div class="flex items-center justify-center w-12 bg-blue-500">
<svg class="w-6 h-6 text-white fill-current" viewBox="0 0 40 40" xmlns="http://www.w3.org/2000/svg">
<path
d="M20 3.33331C10.8 3.33331 3.33337 10.8 3.33337 20C3.33337 29.2 10.8 36.6666 20 36.6666C29.2 36.6666 36.6667 29.2 36.6667 20C36.6667 10.8 29.2 3.33331 20 3.33331ZM21.6667 28.3333H18.3334V25H21.6667V28.3333ZM21.6667 21.6666H18.3334V11.6666H21.6667V21.6666Z"
/>
</svg>
</div>
<div class="px-4 py-2 -mx-3">
<div class="mx-3">
<span class="font-semibold text-blue-500">{{ notification.title }}</span>
<p class="text-sm text-gray-600">T{{ notification.text }}</p>
</div>
</div>
</div>
<div
class="flex w-full max-w-sm mx-auto mt-4 overflow-hidden bg-white rounded-lg shadow-md"
v-if="notification.type === 'warning'"
>
<div class="flex items-center justify-center w-12 bg-yellow-500">
<svg class="w-6 h-6 text-white fill-current" viewBox="0 0 40 40" xmlns="http://www.w3.org/2000/svg">
<path
d="M20 3.33331C10.8 3.33331 3.33337 10.8 3.33337 20C3.33337 29.2 10.8 36.6666 20 36.6666C29.2 36.6666 36.6667 29.2 36.6667 20C36.6667 10.8 29.2 3.33331 20 3.33331ZM21.6667 28.3333H18.3334V25H21.6667V28.3333ZM21.6667 21.6666H18.3334V11.6666H21.6667V21.6666Z"
/>
</svg>
</div>
<div class="px-4 py-2 -mx-3">
<div class="mx-3">
<span class="font-semibold text-yellow-500">{{ notification.title }}</span>
<p class="text-sm text-gray-600">{{ notification.text }}</p>
</div>
</div>
</div>
</div>
</Notification>
</div>
</div>
</NotificationGroup>
</template>

View file

@ -1,56 +1,42 @@
<script setup>
import { computed } from 'vue'
import { colorsBgLight, colorsOutline } from '@/colors.js'
import BaseIcon from '@/Components/BaseIcon.vue'
import { computed } from 'vue';
import { colorsBgLight, colorsOutline } from '@/colors.js';
import BaseIcon from '@/Components/BaseIcon.vue';
const props = defineProps({
text: {
type: String,
required: true
},
type: {
type: String,
required: true
},
icon: {
type: String,
default: null
},
small: Boolean,
outline: Boolean,
wrapped: Boolean
})
text: {
type: String,
required: true,
},
type: {
type: String,
required: true,
},
icon: {
type: String,
default: null,
},
small: Boolean,
outline: Boolean,
wrapped: Boolean,
});
const componentClass = computed(() => {
const baseColor = props.outline ? colorsOutline[props.type] : colorsBgLight[props.type]
const baseColor = props.outline ? colorsOutline[props.type] : colorsBgLight[props.type];
const base = [
'border',
props.small ? 'py-1 px-4 text-xs rounded-full' : 'py-2 px-6 rounded-full',
baseColor
]
const base = ['border', props.small ? 'py-1 px-4 text-xs rounded-full' : 'py-2 px-6 rounded-full', baseColor];
if (!props.wrapped) {
base.push(props.small ? 'mr-1.5' : 'mr-3', 'last:mr-0')
}
if (!props.wrapped) {
base.push(props.small ? 'mr-1.5' : 'mr-3', 'last:mr-0');
}
return base
})
return base;
});
</script>
<template>
<div
class="inline-flex items-center capitalize"
:class="componentClass"
>
<BaseIcon
v-if="icon"
:path="icon"
h="h-4"
w="w-4"
:class="small ? 'mr-1' : 'mr-2'"
:size="small ? 14 : 16"
/>
<span>{{ text }}</span>
</div>
<div class="inline-flex items-center capitalize" :class="componentClass">
<BaseIcon v-if="icon" :path="icon" h="h-4" w="w-4" :class="small ? 'mr-1' : 'mr-2'" :size="small ? 14 : 16" />
<span>{{ text }}</span>
</div>
</template>

View file

@ -17,68 +17,72 @@
</ul> -->
<!-- <div class="flex-col justify-center relative"> -->
<div class="relative">
<input
v-model="data.search"
type="text"
:class="inputElClass"
:name="props.name"
:placeholder="placeholder"
autocomplete="off"
@keydown.down="onArrowDown"
@keydown.up="onArrowUp"
/>
<svg
class="w-4 h-4 absolute left-2.5 top-3.5"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
</svg>
</div>
<!-- <ul v-if="data.isOpen" class="bg-white border border-gray-100 w-full mt-2 max-h-28 overflow-y-auto"> -->
<!-- :ref="(el) => { ul[i] = el }" -->
<ul v-if="data.isOpen" class="bg-white dark:bg-slate-800 w-full mt-2 max-h-28 overflow-y-auto scroll-smooth">
<li
class="pl-8 pr-2 py-1 border-b-2 border-gray-100 relative cursor-pointer hover:bg-yellow-50 hover:text-gray-900"
:class="{
'bg-yellow-50 text-gray-900': i == selectedIndex,
}"
v-for="(result, i) in data.results"
:key="i"
:ref="
(el: HTMLLIElement | null) => {
<div class="mb-6 mx-2 mt-2">
<div class="relative">
<input
v-model="data.search"
type="text"
:class="inputElClass"
:name="props.name"
:placeholder="placeholder"
autocomplete="off"
@keydown.down="onArrowDown"
@keydown.up="onArrowUp"
@keydown.enter="onEnter"
/>
<svg
class="w-4 h-4 absolute left-2.5 top-3.5"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
</svg>
</div>
<!-- <ul v-if="data.isOpen" class="bg-white border border-gray-100 w-full mt-2 max-h-28 overflow-y-auto"> -->
<!-- :ref="(el) => { ul[i] = el }" -->
<ul v-if="data.isOpen" class="bg-white dark:bg-slate-800 w-full mt-2 max-h-28 overflow-y-auto scroll-smooth">
<li
class="pl-8 pr-2 py-1 border-b-2 border-gray-100 relative cursor-pointer hover:bg-yellow-50 hover:text-gray-900"
:class="{
'bg-yellow-50 text-gray-900': i == selectedIndex,
}"
v-for="(result, i) in data.results"
:key="i"
@click.prevent="setResult(result)"
:ref="
(el: HTMLLIElement) => {
ul[i] = el;
}
"
>
<svg class="absolute w-4 h-4 left-2 top-2" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor">
<path
fill-rule="evenodd"
d="M12.293 5.293a1 1 0 011.414 0l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414-1.414L14.586 11H3a1 1 0 110-2h11.586l-2.293-2.293a1 1 0 010-1.414z"
clip-rule="evenodd"
/>
</svg>
<!-- <b>Gar</b>{{ result.name }} -->
>
<svg class="absolute w-4 h-4 left-2 top-2" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor">
<path
fill-rule="evenodd"
d="M12.293 5.293a1 1 0 011.414 0l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414-1.414L14.586 11H3a1 1 0 110-2h11.586l-2.293-2.293a1 1 0 010-1.414z"
clip-rule="evenodd"
/>
</svg>
<!-- <b>Gar</b>{{ result.name }} -->
<!-- <span>
<!-- <span>
{{ makeBold(result.name) }}
</span> -->
<span
v-for="(item, index) in makeBold(result.name)"
:key="index"
:class="{
'font-bold': data.search.toLowerCase().includes(item.toLowerCase()),
'is-active': index == selectedIndex,
}"
>
{{ item }}
</span>
</li>
</ul>
<!-- </div> -->
<span
v-for="(item, index) in makeBold(result.name)"
:key="index"
:class="{
'font-bold': data.search.toLowerCase().includes(item.toLowerCase()),
'is-active': index == selectedIndex,
}"
>
{{ item }}
</span>
<span> - {{ result.email }}</span>
</li>
</ul>
</div>
</template>
<script setup lang="ts">
@ -119,6 +123,8 @@ let props = defineProps({
ctrlKFocus: Boolean,
});
const emit = defineEmits(['person']);
const inputElClass = computed(() => {
const base = [
'px-3 py-2 max-w-full focus:ring focus:outline-none border-gray-700 rounded w-full',
@ -143,11 +149,11 @@ let data = reactive({
let error = ref('');
let selectedIndex: Ref<number> = ref(0);
// const listItem = ref(null);
const ul: Ref<Array<HTMLLIElement | null>> = ref([]);
const ul: Ref<Array<HTMLLIElement>> = ref([]);
watch(selectedIndex, (selectedIndex) => {
watch(selectedIndex, (selectedIndex: number) => {
if (selectedIndex != null && ul.value != null) {
const currentElement: HTMLLIElement | null = ul.value[selectedIndex];
const currentElement: HTMLLIElement = ul.value[selectedIndex];
currentElement &&
currentElement.scrollIntoView({
behavior: 'smooth',
@ -161,13 +167,6 @@ watch(search, async () => {
await onChange();
});
// function clear() {
// data.search = "";
// data.isOpen = false;
// data.results = [];
// error.value = "";
// // this.$emit("clear");
// }
// function onChange() {
// if (!props.source || !data.search) {
@ -278,4 +277,30 @@ function onArrowUp() {
selectedIndex.value = selectedIndex.value == 0 || selectedIndex.value == -1 ? data.results.length - 1 : selectedIndex.value - 1;
}
}
function setResult(person) {
// this.search = person.full_name;
clear();
// this.$emit('person', person);
emit('person', person);
}
function clear() {
data.search = '';
data.isOpen = false;
data.results = [];
error.value = '';
// this.$emit("clear");
}
function onEnter() {
if (Array.isArray(data.results) && data.results.length && selectedIndex.value !== -1 && selectedIndex.value < data.results.length) {
//this.display = this.results[this.selectedIndex];
const person = data.results[selectedIndex.value];
// this.$emit('person', person);
emit('person', person);
clear();
selectedIndex.value = -1;
}
}
</script>

View file

@ -0,0 +1,157 @@
<script setup lang="ts">
import { computed, ref } from 'vue';
// import { MainService } from '@/Stores/main';
import { StyleService } from '@/Stores/style';
import { mdiTrashCan } from '@mdi/js';
// import CardBoxModal from '@/Components/CardBoxModal.vue';
// import TableCheckboxCell from '@/Components/TableCheckboxCell.vue';
import BaseLevel from '@/Components/BaseLevel.vue';
import BaseButtons from '@/Components/BaseButtons.vue';
import BaseButton from '@/Components/BaseButton.vue';
import UserAvatar from '@/Components/UserAvatar.vue';
// import Person from 'App/Models/Person';
import { Person } from '@/Stores/main';
const props = defineProps({
checkable: Boolean,
persons: {
type: Array<Person>,
default: () => [],
},
});
const styleService = StyleService();
// const mainService = MainService();
const items = computed(() => props.persons);
// const isModalActive = ref(false);
// const isModalDangerActive = ref(false);
const perPage = ref(5);
const currentPage = ref(0);
// const checkedRows = ref([]);
const itemsPaginated = computed(() => items.value.slice(perPage.value * currentPage.value, perPage.value * (currentPage.value + 1)));
const numPages = computed(() => Math.ceil(items.value.length / perPage.value));
const currentPageHuman = computed(() => currentPage.value + 1);
const pagesList = computed(() => {
const pagesList: Array<number> = [];
for (let i = 0; i < numPages.value; i++) {
pagesList.push(i);
}
return pagesList;
});
const removeAuthor = (key) => {
items.value.splice(key, 1);
};
// const remove = (arr, cb) => {
// const newArr = [];
// arr.forEach((item) => {
// if (!cb(item)) {
// newArr.push(item);
// }
// });
// return newArr;
// };
// const checked = (isChecked, client) => {
// if (isChecked) {
// checkedRows.value.push(client);
// } else {
// checkedRows.value = remove(checkedRows.value, (row) => row.id === client.id);
// }
// };
</script>
<template>
<!-- <CardBoxModal v-model="isModalActive" title="Sample modal">
<p>Lorem ipsum dolor sit amet <b>adipiscing elit</b></p>
<p>This is sample modal</p>
</CardBoxModal>
<CardBoxModal v-model="isModalDangerActive" large-title="Please confirm" button="danger" has-cancel>
<p>Lorem ipsum dolor sit amet <b>adipiscing elit</b></p>
<p>This is sample modal</p>
</CardBoxModal> -->
<!-- <div v-if="checkedRows.length" class="p-3 bg-gray-100/50 dark:bg-slate-800">
<span v-for="checkedRow in checkedRows" :key="checkedRow.id"
class="inline-block px-2 py-1 rounded-sm mr-2 text-sm bg-gray-100 dark:bg-slate-700">
{{ checkedRow.name }}
</span>
</div> -->
<table>
<thead>
<tr>
<!-- <th v-if="checkable" /> -->
<th class="hidden lg:table-cell"></th>
<th>Name</th>
<th>Email</th>
<!-- <th>Name Type</th> -->
<!-- <th>Progress</th> -->
<th>Created</th>
<th />
</tr>
</thead>
<tbody>
<tr v-for="(client, index) in itemsPaginated" :key="client.id">
<!-- <TableCheckboxCell v-if="checkable" @checked="checked($event, client)" /> -->
<td class="border-b-0 lg:w-6 before:hidden hidden lg:table-cell">
<UserAvatar :username="client.name" class="w-24 h-24 mx-auto lg:w-6 lg:h-6" />
</td>
<td data-label="Name">
{{ client.name }}
</td>
<td data-label="Email">
{{ client.email }}
</td>
<!-- <td data-label="Name Type">
{{ client.name_type }}
</td> -->
<!-- <td data-label="Orcid">
{{ client.identifier_orcid }}
</td> -->
<!-- <td data-label="Progress" class="lg:w-32">
<progress class="flex w-2/5 self-center lg:w-full" max="100" v-bind:value="client.progress">
{{ client.progress }}
</progress>
</td> -->
<td data-label="Created" class="lg:w-1 whitespace-nowrap">
<small class="text-gray-500 dark:text-slate-400" :title="client.created_at">{{ client.created_at }}</small>
</td>
<td class="before:hidden lg:w-1 whitespace-nowrap">
<BaseButtons type="justify-start lg:justify-end" no-wrap>
<!-- <BaseButton color="info" :icon="mdiEye" small @click="isModalActive = true" /> -->
<BaseButton color="danger" :icon="mdiTrashCan" small @click.prevent="removeAuthor(index)" />
</BaseButtons>
</td>
</tr>
</tbody>
</table>
<!-- :class="[ pagesList.length > 1 ? 'block' : 'hidden']" -->
<div class="p-3 lg:px-6 border-t border-gray-100 dark:border-slate-800" >
<BaseLevel>
<BaseButtons>
<BaseButton
v-for="page in pagesList"
:key="page"
:active="page === currentPage"
:label="page + 1"
small
:outline="styleService.darkMode"
@click="currentPage = page"
/>
</BaseButtons>
<small>Page {{ currentPageHuman }} of {{ numPages }}</small>
</BaseLevel>
</div>
</template>

View file

@ -1,15 +1,12 @@
<script setup>
import { computed } from 'vue'
import { computed } from 'vue';
// import { usePage } from '@inertiajs/vue3'
import { usePage } from '@inertiajs/vue3'
import UserAvatar from '@/Components/UserAvatar.vue'
import { usePage } from '@inertiajs/vue3';
import UserAvatar from '@/Components/UserAvatar.vue';
const userName = computed(() => usePage().props.auth?.user.name)
const userName = computed(() => usePage().props.auth?.user.name);
</script>
<template>
<UserAvatar
v-bind:username="'userName'"
api="initials"
/>
</template>
<UserAvatar v-bind:username="'userName'" api="initials" />
</template>