feat: update API controllers, validations, and Vue components
All checks were successful
CI / container-job (push) Successful in 49s

- Modified Api/Authors.Controller.ts to use only personal types and sort by dataset_count.
- Completely rewritten AvatarController.ts.
- Added new Api/CollectionsController.ts for querying collections and collection_roles.
- Modified Api/DatasetController.ts to preload titles, identifier and order by server_date_published.
- Modified FileController.ts to serve files from /storage/app/data/ instead of /storage/app/public.
- Added new Api/UserController for requesting submitters (getSubmitters).
- Improved OaiController.ts with performant DB queries for better ResumptionToken handling.
- Modified Submitter/DatasetController.ts by adding a categorize method for library classification.
- Rewritten ResumptionToken.ts.
- Improved TokenWorkerService.ts to utilize browser fingerprint.
- Edited dataset.ts by adding the doiIdentifier property.
- Enhanced person.ts to improve the fullName property.
- Completely rewritten AsideMenuItem.vue component.
- Updated CarBoxClient.vue to use TypeScript.
- Added new CardBoxDataset.vue for displaying recent datasets on the dashboard.
- Completely rewritten TableSampleClients.vue for the dashboard.
- Completely rewritten UserAvatar.vue.
- Made small layout changes in Dashboard.vue.
- Added new Category.vue for browsing scientific collections.
- Adapted the pinia store in main.ts.
- Added additional routes in start/routes.ts and start/api/routes.ts.
- Improved referenceValidation.ts for better ISBN existence checking.
- NPM dependency updates.
This commit is contained in:
Kaimbacher 2025-03-14 17:39:58 +01:00
parent 36cd7a757b
commit b540547e4c
34 changed files with 1757 additions and 1018 deletions

View file

@ -1,162 +1,143 @@
<script lang="ts" setup>
import { computed, ComputedRef } from 'vue';
import { computed } from 'vue';
import { Link, usePage } from '@inertiajs/vue3';
// import { Link } from '@inertiajs/inertia-vue3';
import { StyleService } from '@/Stores/style.service';
import { mdiMinus, mdiPlus } from '@mdi/js';
import { getButtonColor } from '@/colors';
import BaseIcon from '@/Components/BaseIcon.vue';
// import AsideMenuList from '@/Components/AsideMenuList.vue';
import { stardust } from '@eidellev/adonis-stardust/client';
import type { User } from '@/Dataset';
import { MenuItem } from '@headlessui/vue';
const props = defineProps({
item: {
type: Object,
required: true,
},
parentItem: {
type: Object,
required: false,
},
// isDropdownList: Boolean,
});
interface MenuItem {
href?: string;
route?: string;
icon?: string;
label: string;
target?: string;
color?: string;
children?: MenuItem[];
isOpen?: boolean;
roles?: string[];
}
const user: ComputedRef<User> = computed(() => {
return usePage().props.authUser as User;
const props = defineProps<{
item: MenuItem;
parentItem?: MenuItem;
// isDropdownList?: boolean;
}>();
const emit = defineEmits<{
(e: 'menu-click', event: Event, item: MenuItem): void;
}>();
// Retrieve authenticated user from page props
const user = computed<User>(() => usePage().props.authUser as User);
// Check if the menu item has children
const hasChildren = computed(() => {
return Array.isArray(props.item?.children) && props.item.children.length > 0;
});
const itemRoute = computed(() => (props.item && props.item.route ? stardust.route(props.item.route) : ''));
// const isCurrentRoute = computed(() => (props.item && props.item.route ? stardust.isCurrent(props.item.route): false));
// const itemHref = computed(() => (props.item && props.item.href ? props.item.href : ''));
const emit = defineEmits(['menu-click']);
// Determine which element to render based on 'href' or 'route'
const isComponent = computed(() => {
if (props.item.href) {
return 'a';
}
if (props.item.route) {
return Link;
}
return 'div';
});
// Check if any child route is active
const isChildActive = computed(() => {
if (props.item.children && props.item.children.length > 0) {
return props.item.children.some(child => child.route && stardust.isCurrent(child.route));
}
return false;
});
// Automatically use prop item.isOpen if set from the parent,
// or if one of its children is active then force open state.
const isOpen = computed(() => {
return props.item.isOpen || isChildActive.value;
});
const styleService = StyleService();
const hasColor = computed(() => props.item && props.item.color);
// const isDropdownOpen = ref(false);
// const isChildSelected = computed(() => {
// if (props.item.children && props.item.children.length > 0) {
// return children.value.some(childItem => stardust.isCurrent(childItem.route));
// }
// return false;
// const children = computed(() => {
// return props.item.children || [];
// });
const hasChildren = computed(() => {
// props.item.children?.length > 0
if (props.item.children && props.item.children.length > 0) {
return true;
}
return false;
});
const children = computed(() => {
return props.item.children || [];
});
const componentClass = computed(() => [
hasChildren ? 'py-3 px-6 text-sm font-semibold' : 'py-3 px-6',
hasColor.value ? getButtonColor(props.item.color, false, true) : styleService.asideMenuItemStyle,
]);
// const toggleDropdown = () => {
// // emit('menu-click', event, props.item);
// // console.log(props.item);
// if (hasChildren.value) {
// isDropdownOpen.value = !isDropdownOpen.value;
// }
// // if (props.parentItem?.hasDropdown.value) {
// // props.parentItem.isDropdownActive.value = true;
// // }
// };
const menuClick = (event) => {
const menuClick = (event: Event) => {
emit('menu-click', event, props.item);
if (hasChildren.value) {
// if (isChildSelected.value == false) {
// isDropdownOpen.value = !isDropdownOpen.value;
props.item.isOpen = !props.item.isOpen;
// }
// Toggle open state if the menu has children
props.item.isOpen = !props.item.isOpen;
}
};
// const handleChildSelected = () => {
// isChildSelected.value = true;
// };
const activeInactiveStyle = computed(() => {
const activeStyle = computed(() => {
if (props.item.route && stardust.isCurrent(props.item.route)) {
// console.log(props.item.route);
return styleService.asideMenuItemActiveStyle;
return 'text-sky-600 font-bold';
} else {
return null;
}
});
const is = computed(() => {
if (props.item.href) {
return 'a';
}
if (props.item.route) {
return Link;
}
return 'div';
});
const hasRoles = computed(() => {
if (props.item.roles) {
return user.value.roles.some(role => props.item.roles.includes(role.name));
return user.value.roles.some(role => props.item.roles?.includes(role.name));
// return test;
}
return true
});
// props.routeName && stardust.isCurrent(props.routeName) ? props.activeColor : null
</script>
<!-- :target="props.item.target ?? null" -->
<template>
<li v-if="hasRoles">
<!-- <component :is="itemHref ? 'div' : Link" :href="itemHref ? itemHref : itemRoute" -->
<component :is="is" :href="itemRoute ? stardust.route(props.item.route) : props.item.href"
class="flex cursor-pointer dark:text-slate-300 dark:hover:text-white menu-item-wrapper" :class="componentClass"
@click="menuClick" v-bind:target="props.item.target ?? null">
<BaseIcon v-if="item.icon" :path="item.icon" class="flex-none menu-item-icon" :class="activeInactiveStyle"
w="w-16" :size="18" />
<component :is="isComponent" :href="props.item.href ? props.item.href : itemRoute"
class="flex cursor-pointer dark:text-slate-300 dark:hover:text-white menu-item-wrapper"
:class="componentClass" @click="menuClick" :target="props.item.target || null">
<BaseIcon v-if="props.item.icon" :path="props.item.icon" class="flex-none menu-item-icon"
:class="activeStyle" w="w-16" :size="18" />
<div class="menu-item-label">
<span class="grow text-ellipsis line-clamp-1" :class="activeInactiveStyle">
{{ item.label }}
<span class="grow text-ellipsis line-clamp-1" :class="[activeStyle]">
{{ props.item.label }}
</span>
</div>
<!-- plus icon for expanding sub menu -->
<BaseIcon v-if="hasChildren" :path="props.item.isOpen ? mdiMinus : mdiPlus" class="flex-none"
:class="[activeInactiveStyle]" w="w-12" />
<!-- Display plus or minus icon if there are child items -->
<BaseIcon v-if="hasChildren" :path="isOpen ? mdiMinus : mdiPlus" class="flex-none"
:class="[activeStyle]" w="w-12" />
</component>
<!-- Render dropdown -->
<div class="menu-item-dropdown"
:class="[styleService.asideMenuDropdownStyle, props.item.isOpen ? 'block dark:bg-slate-800/50' : 'hidden']"
v-if="hasChildren">
:class="[styleService.asideMenuDropdownStyle, isOpen ? 'block dark:bg-slate-800/50' : 'hidden']"
v-if="props.item.children && props.item.children.length > 0">
<ul>
<!-- <li v-for="( child, index ) in children " :key="index">
<AsideMenuItem :item="child" :key="index"> </AsideMenuItem>
</li> -->
<AsideMenuItem v-for="(childItem, index) in children" :key="index" :item="childItem" />
<AsideMenuItem v-for="(childItem, index) in (props.item.children as any[])" :key="index" :item="childItem"
@menu-click="$emit('menu-click', $event, childItem)" />
</ul>
</div>
<!-- <AsideMenuList v-if="hasChildren" :items="item.children"
:class="[styleService.asideMenuDropdownStyle, isDropdownOpen ? 'block dark:bg-slate-800/50' : 'hidden']" /> -->
</li>
</div>
</li>
</template>
<style>
@ -167,17 +148,12 @@ const hasRoles = computed(() => {
}
.menu-item-icon {
font-size: 2.5rem;
/* margin-right: 10px; */
font-size: 2.5rem;
/* margin-right: 10px; */
}
/* .menu-item-label {
font-size: 1.2rem;
font-weight: bold;
} */
.menu-item-dropdown {
/* margin-left: 10px; */
padding-left: 0.75rem;
/* margin-left: 10px; */
padding-left: 0.75rem;
}
</style>

View file

@ -1,6 +1,6 @@
<script setup>
<script lang="ts" setup>
import { computed } from 'vue';
import { mdiTrendingDown, mdiTrendingUp, mdiTrendingNeutral } from '@mdi/js';
// import { mdiTrendingDown, mdiTrendingUp, mdiTrendingNeutral } from '@mdi/js';
import CardBox from '@/Components/CardBox.vue';
import BaseLevel from '@/Components/BaseLevel.vue';
import PillTag from '@/Components/PillTag.vue';
@ -27,6 +27,10 @@ const props = defineProps({
type: Number,
default: 0,
},
count: {
type: Number,
default: 0,
},
text: {
type: String,
default: null,
@ -42,11 +46,11 @@ const pillType = computed(() => {
return props.type;
}
if (props.progress) {
if (props.progress >= 60) {
if (props.count) {
if (props.count >= 20) {
return 'success';
}
if (props.progress >= 40) {
if (props.count >= 5) {
return 'warning';
}
@ -56,17 +60,17 @@ const pillType = computed(() => {
return 'info';
});
const pillIcon = computed(() => {
return {
success: mdiTrendingUp,
warning: mdiTrendingNeutral,
danger: mdiTrendingDown,
info: mdiTrendingNeutral,
}[pillType.value];
});
// const pillIcon = computed(() => {
// return {
// success: mdiTrendingUp,
// warning: mdiTrendingNeutral,
// danger: mdiTrendingDown,
// info: mdiTrendingNeutral,
// }[pillType.value];
// });
const pillText = computed(() => props.text ?? `${props.progress}%`);
</script>
// const pillText = computed(() => props.text ?? `${props.progress}%`);
// </script>
<template>
<CardBox class="mb-6 last:mb-0" hoverable>
@ -83,7 +87,17 @@ const pillText = computed(() => props.text ?? `${props.progress}%`);
</p>
</div>
</BaseLevel>
<PillTag :type="pillType" :text="pillText" small :icon="pillIcon" />
<!-- <PillTag :type="pillType" :text="text" small :icon="pillIcon" /> -->
<div class="text-center md:text-right space-y-2">
<p class="text-sm text-gray-500">
Count
</p>
<div>
<PillTag :type="pillType" :text="String(count)" small />
</div>
</div>
</BaseLevel>
</CardBox>
</template>

View file

@ -0,0 +1,107 @@
<script lang="ts" setup>
import { computed, PropType } from 'vue';
import { mdiChartTimelineVariant, mdiFileDocumentOutline, mdiFileOutline, mdiDatabase } from '@mdi/js';
import CardBox from '@/Components/CardBox.vue';
import PillTag from '@/Components/PillTag.vue';
import IconRounded from '@/Components/IconRounded.vue';
import dayjs from 'dayjs';
import relativeTime from 'dayjs/plugin/relativeTime';
// Extend dayjs to support relative times
dayjs.extend(relativeTime);
interface Dataset {
account_id: number;
created_at: string;
creating_corporation: string;
editor_id: number;
embargo_date: string | null;
id: number;
language: string;
main_abstract: string | null;
main_title: string | null;
preferred_reviewer: string | null;
preferred_reviewer_email: string | null;
project_id: number | null;
publish_id: number;
publisher_name: string;
reject_editor_note: string | null;
reject_reviewer_note: string | null;
remaining_time: number;
reviewer_id: number;
server_date_modified: string;
server_date_published: string;
server_state: string;
type: string;
doi_identifier: string;
}
const props = defineProps({
dataset: {
type: Object as PropType<Dataset>,
required: true
}
});
const icon = computed(() => {
switch (props.dataset.type) {
case 'analysisdata':
return { icon: mdiChartTimelineVariant, type: 'success' };
case 'measurementdata':
return { icon: mdiFileDocumentOutline, type: 'warning' };
case 'monitoring':
return { icon: mdiFileOutline, type: 'info' };
case 'remotesensing':
return { icon: mdiDatabase, type: 'primary' };
case 'gis':
return { icon: mdiDatabase, type: 'info' };
case 'models':
return { icon: mdiChartTimelineVariant, type: 'success' };
case 'mixedtype':
return { icon: mdiFileDocumentOutline, type: 'warning' };
case 'vocabulary':
return { icon: mdiFileOutline, type: 'info' };
default:
return { icon: mdiDatabase, type: 'secondary' };
}
});
const displayTitle = computed(() => props.dataset.main_title || 'Untitled Dataset');
const doiLink = computed(() => {
return `https://doi.tethys.at/10.24341/tethys.${props.dataset.publish_id}`;
});
const relativeDate = computed(() => {
const publishedDate = dayjs(props.dataset.server_date_published);
if (publishedDate.isValid()) {
return publishedDate.fromNow();
}
return props.dataset.server_date_published;
});
// const displayBusiness = computed(() => props.dataset.publisher_name);
</script>
<template>
<CardBox class="mb-6 last:mb-0" hoverable>
<div class="flex items-start justify-between">
<IconRounded :icon="icon.icon" :type="icon.type" class="mr-6" />
<div class="flex-grow space-y-1 text-left" style="width: 70%;">
<h4 class="text-lg truncate" >
{{ displayTitle }}
</h4>
<p class="text-gray-500 dark:text-slate-400">
<b>
<a :href="doiLink" target="_blank">View Publication</a>
</b>
{{ relativeDate }}
</p>
</div>
<div class="flex flex-col items-end gap-2">
<p class="text-sm text-gray-500">{{ props.dataset.type }}</p>
<PillTag :type="icon.type" :text="props.dataset.type" small class="inline-flex" />
</div>
</div>
</CardBox>
</template>

View file

@ -1,17 +1,19 @@
<script setup>
import { computed, ref } from 'vue';
<script lang="ts" setup>
import { computed, ref, Ref } from 'vue';
import { MainService } from '@/Stores/main';
import { StyleService } from '@/Stores/style.service';
import { mdiEye, mdiTrashCan } from '@mdi/js';
import { mdiEye } 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 dayjs from 'dayjs';
import { User } from '@/Stores/main';
defineProps({
checkable: Boolean,
checkable: Boolean,
});
const styleService = StyleService();
@ -19,128 +21,124 @@ const mainService = MainService();
const items = computed(() => mainService.clients);
const isModalActive = ref(false);
const isModalDangerActive = ref(false);
// const isModalDangerActive = ref(false);
const perPage = ref(5);
const currentPage = ref(0);
const checkedRows = ref([]);
const currentClient: Ref<User | null> = ref(null);
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 = [];
for (let i = 0; i < numPages.value; i++) {
pagesList.push(i);
}
return pagesList;
const pagesList = [];
for (let i = 0; i < numPages.value; i++) {
pagesList.push(i);
}
return pagesList;
});
const remove = (arr, cb) => {
const newArr = [];
arr.forEach((item) => {
if (!cb(item)) {
newArr.push(item);
}
});
return newArr;
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);
}
if (isChecked) {
checkedRows.value.push(client);
} else {
checkedRows.value = remove(checkedRows.value, (row) => row.id === client.id);
}
};
const showModal = (client: User) => {
currentClient.value = client;
isModalActive.value = true;
};
</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>
<CardBoxModal v-model="isModalActive" :title="currentClient ? currentClient.login : ''">
<div v-if="currentClient">
<p>Login: {{ currentClient.login }}</p>
<p>Email: {{ currentClient.email }}</p>
<p>Created: {{ currentClient?.created_at ? dayjs(currentClient.created_at).format('MMM D, YYYY h:mm A') : 'N/A' }}
</p>
</div>
</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> -->
<table>
<thead>
<tr>
<th v-if="checkable" />
<th />
<th>Name</th>
<th>Email</th>
<th>City</th>
<th>Progress</th>
<th>Created</th>
<th />
</tr>
</thead>
<tbody>
<tr v-for="client in itemsPaginated" :key="client.id">
<TableCheckboxCell v-if="checkable" @checked="checked($event, client)" />
<td class="border-b-0 lg:w-6 before:hidden">
<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="City">
{{ client.city }}
</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">{{ client.created }}</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="isModalDangerActive = true" />
</BaseButtons>
</td>
</tr>
</tbody>
</table>
<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>
<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.login }}
</span>
</div>
<table>
<thead>
<tr>
<th v-if="checkable" />
<th />
<th>Login</th>
<th>Email</th>
<th>Created</th>
<th />
</tr>
</thead>
<tbody>
<tr v-for="client in itemsPaginated" :key="client.id">
<TableCheckboxCell v-if="checkable" @checked="checked($event, client)" />
<td class="border-b-0 lg:w-6 before:hidden">
<!-- <UserAvatar :username="client.login" :avatar="client.avatar" class="w-24 h-24 mx-auto lg:w-6 lg:h-6" /> -->
<div v-if="client.avatar">
<UserAvatar :default-url="client.avatar ? '/public' + client.avatar : ''"
:username="client.first_name + ' ' + client.last_name" class="w-24 h-24 mx-auto lg:w-6 lg:h-6" />
</div>
<div v-else>
<UserAvatar :username="client.first_name + ' ' + client.last_name" class="w-24 h-24 mx-auto lg:w-6 lg:h-6" />
</div>
</td>
<td data-label="Login">
{{ client.login }}
</td>
<td data-label="Email">
{{ client.email }}
</td>
<td data-label="Created">
<small class="text-gray-500 dark:text-slate-400"
:title="client.created_at ? dayjs(client.created_at).format('MMM D, YYYY h:mm A') : 'N/A'">
{{ client.created_at ? dayjs(client.created_at).format('MMM D, YYYY h:mm A') : 'N/A' }}
</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="showModal(client)" />
<!-- <BaseButton color="danger" :icon="mdiTrashCan" small @click="isModalDangerActive = true" /> -->
</BaseButtons>
</td>
</tr>
</tbody>
</table>
<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,4 +1,4 @@
<script setup>
<script lang="ts" setup>
import { computed } from 'vue';
const props = defineProps({
@ -22,55 +22,55 @@ const avatar = computed(() => {
const username = computed(() => props.username);
const darkenColor = (color) => {
const r = parseInt(color.slice(0, 2), 16);
const g = parseInt(color.slice(2, 4), 16);
const b = parseInt(color.slice(4, 6), 16);
// const darkenColor = (color: string) => {
// const r = parseInt(color.slice(0, 2), 16);
// const g = parseInt(color.slice(2, 4), 16);
// const b = parseInt(color.slice(4, 6), 16);
const darkerR = Math.round(r * 0.6);
const darkerG = Math.round(g * 0.6);
const darkerB = Math.round(b * 0.6);
// const darkerR = Math.round(r * 0.6);
// const darkerG = Math.round(g * 0.6);
// const darkerB = Math.round(b * 0.6);
const darkerColor = ((darkerR << 16) + (darkerG << 8) + darkerB).toString(16);
// const darkerColor = ((darkerR << 16) + (darkerG << 8) + darkerB).toString(16);
return darkerColor.padStart(6, '0');
};
// return darkerColor.padStart(6, '0');
// };
const getColorFromName = (name) => {
let hash = 0;
for (let i = 0; i < name.length; i++) {
hash = name.charCodeAt(i) + ((hash << 5) - hash);
}
let color = '#';
for (let i = 0; i < 3; i++) {
const value = (hash >> (i * 8)) & 0xff;
color += ('00' + value.toString(16)).substr(-2);
}
return color.replace('#', '');
};
// const getColorFromName = (name: string): string => {
// let hash = 0;
// for (let i = 0; i < name.length; i++) {
// hash = name.charCodeAt(i) + ((hash << 5) - hash);
// }
// let color = '#';
// for (let i = 0; i < 3; i++) {
// const value = (hash >> (i * 8)) & 0xff;
// color += ('00' + value.toString(16)).substr(-2);
// }
// return color.replace('#', '');
// };
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);
// const lightenColor = (hexColor: string, percent: number): string => {
// 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 = 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;
// r = (r < 255) ? r : 255;
// g = (g < 255) ? g : 255;
// b = (b < 255) ? b : 255;
const lighterHex = ((r << 16) | (g << 8) | b).toString(16);
// const lighterHex = ((r << 16) | (g << 8) | b).toString(16);
return lighterHex.padStart(6, '0');
};
// return lighterHex.padStart(6, '0');
// };
const generateAvatarUrl = (name) => {
const originalColor = getColorFromName(name);
const backgroundColor = lightenColor(originalColor, 60);
const textColor = darkenColor(originalColor);
const generateAvatarUrl = (name: string): string => {
// const originalColor = getColorFromName(name);
// const backgroundColor = lightenColor(originalColor, 60);
// const textColor = darkenColor(originalColor);
const avatarUrl = `/api/avatar?name=${name}&size=50`;
return avatarUrl;