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.
135 lines
5 KiB
TypeScript
135 lines
5 KiB
TypeScript
import type { HttpContext } from '@adonisjs/core/http';
|
|
import { StatusCodes } from 'http-status-codes';
|
|
import redis from '@adonisjs/redis/services/main';
|
|
|
|
const PREFIXES = ['von', 'van'];
|
|
const DEFAULT_SIZE = 50;
|
|
const FONT_SIZE_RATIO = 0.4;
|
|
const COLOR_LIGHTENING_PERCENT = 60;
|
|
const COLOR_DARKENING_FACTOR = 0.6;
|
|
|
|
export default class AvatarController {
|
|
public async generateAvatar({ request, response }: HttpContext) {
|
|
try {
|
|
const { name, size = DEFAULT_SIZE } = request.only(['name', 'size']);
|
|
if (!name) {
|
|
return response.status(StatusCodes.BAD_REQUEST).json({ error: 'Name is required' });
|
|
}
|
|
|
|
// Build a unique cache key for the given name and size
|
|
const cacheKey = `avatar:${name.trim().toLowerCase()}-${size}`;
|
|
const cachedSvg = await redis.get(cacheKey);
|
|
if (cachedSvg) {
|
|
this.setResponseHeaders(response);
|
|
return response.send(cachedSvg);
|
|
}
|
|
|
|
const initials = this.getInitials(name);
|
|
const colors = this.generateColors(name);
|
|
const svgContent = this.createSvg(size, colors, initials);
|
|
|
|
// // Cache the generated avatar for future use, e.g. 1 hour expiry
|
|
await redis.setex(cacheKey, 3600, svgContent);
|
|
|
|
this.setResponseHeaders(response);
|
|
return response.send(svgContent);
|
|
} catch (error) {
|
|
return response.status(StatusCodes.INTERNAL_SERVER_ERROR).json({ error: error.message });
|
|
}
|
|
}
|
|
|
|
private getInitials(name: string): string {
|
|
const parts = name
|
|
.trim()
|
|
.split(' ')
|
|
.filter((part) => part.length > 0);
|
|
|
|
if (parts.length === 0) {
|
|
return 'NA';
|
|
}
|
|
|
|
if (parts.length >= 2) {
|
|
return this.getMultiWordInitials(parts);
|
|
}
|
|
return parts[0].substring(0, 2).toUpperCase();
|
|
}
|
|
|
|
private getMultiWordInitials(parts: string[]): string {
|
|
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()) {
|
|
return firstInitial + lastName.charAt(1).toUpperCase();
|
|
}
|
|
return firstInitial + lastInitial;
|
|
}
|
|
|
|
private generateColors(name: string): { background: string; text: string } {
|
|
const baseColor = this.getColorFromName(name);
|
|
return {
|
|
background: this.lightenColor(baseColor, COLOR_LIGHTENING_PERCENT),
|
|
text: this.darkenColor(baseColor),
|
|
};
|
|
}
|
|
|
|
private createSvg(size: number, colors: { background: string; text: string }, initials: string): string {
|
|
const fontSize = size * FONT_SIZE_RATIO;
|
|
return `
|
|
<svg width="${size}" height="${size}" xmlns="http://www.w3.org/2000/svg">
|
|
<rect width="100%" height="100%" fill="#${colors.background}"/>
|
|
<text x="50%" y="50%" dominant-baseline="middle" text-anchor="middle" font-weight="bold" font-family="Arial, sans-serif" font-size="${fontSize}" fill="#${colors.text}">${initials}</text>
|
|
</svg>
|
|
`;
|
|
}
|
|
|
|
private setResponseHeaders(response: HttpContext['response']): void {
|
|
response.header('Content-type', 'image/svg+xml');
|
|
response.header('Cache-Control', 'no-cache');
|
|
response.header('Pragma', 'no-cache');
|
|
response.header('Expires', '0');
|
|
}
|
|
|
|
private getColorFromName(name: string): string {
|
|
let hash = 0;
|
|
for (let i = 0; i < name.length; i++) {
|
|
hash = name.charCodeAt(i) + ((hash << 5) - hash);
|
|
}
|
|
|
|
const colorParts = [];
|
|
for (let i = 0; i < 3; i++) {
|
|
const value = (hash >> (i * 8)) & 0xff;
|
|
colorParts.push(value.toString(16).padStart(2, '0'));
|
|
}
|
|
return colorParts.join('');
|
|
}
|
|
|
|
private lightenColor(hexColor: string, percent: number): string {
|
|
const r = parseInt(hexColor.substring(0, 2), 16);
|
|
const g = parseInt(hexColor.substring(2, 4), 16);
|
|
const b = parseInt(hexColor.substring(4, 6), 16);
|
|
|
|
const lightenValue = (value: number) => Math.min(255, Math.floor((value * (100 + percent)) / 100));
|
|
|
|
const newR = lightenValue(r);
|
|
const newG = lightenValue(g);
|
|
const newB = lightenValue(b);
|
|
|
|
return ((newR << 16) | (newG << 8) | newB).toString(16).padStart(6, '0');
|
|
}
|
|
|
|
private darkenColor(hexColor: string): string {
|
|
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 darkenValue = (value: number) => Math.round(value * COLOR_DARKENING_FACTOR);
|
|
|
|
const darkerR = darkenValue(r);
|
|
const darkerG = darkenValue(g);
|
|
const darkerB = darkenValue(b);
|
|
|
|
return ((darkerR << 16) + (darkerG << 8) + darkerB).toString(16).padStart(6, '0');
|
|
}
|
|
}
|