mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-14 18:41:08 +02:00
style: auto-format codebase with Prettier
Applied formatting to 1487+ files using pnpm format:write - TypeScript/JavaScript files - Svelte components - Astro pages - JSON configs - Markdown docs 13 files still need manual review (Astro JSX comments)
This commit is contained in:
parent
0241f5554c
commit
d36b321d9d
3952 changed files with 661498 additions and 739751 deletions
|
|
@ -1,22 +1,22 @@
|
|||
{
|
||||
"name": "@manacore/shared-utils",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"description": "Shared utility functions for Manacore monorepo",
|
||||
"main": "./src/index.ts",
|
||||
"types": "./src/index.ts",
|
||||
"exports": {
|
||||
".": "./src/index.ts"
|
||||
},
|
||||
"scripts": {
|
||||
"type-check": "tsc --noEmit",
|
||||
"clean": "rm -rf dist"
|
||||
},
|
||||
"dependencies": {
|
||||
"date-fns": "^4.1.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^24.10.1",
|
||||
"typescript": "^5.9.3"
|
||||
}
|
||||
"name": "@manacore/shared-utils",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"description": "Shared utility functions for Manacore monorepo",
|
||||
"main": "./src/index.ts",
|
||||
"types": "./src/index.ts",
|
||||
"exports": {
|
||||
".": "./src/index.ts"
|
||||
},
|
||||
"scripts": {
|
||||
"type-check": "tsc --noEmit",
|
||||
"clean": "rm -rf dist"
|
||||
},
|
||||
"dependencies": {
|
||||
"date-fns": "^4.1.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^24.10.1",
|
||||
"typescript": "^5.9.3"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,66 +6,61 @@
|
|||
* Sleep for a specified number of milliseconds
|
||||
*/
|
||||
export function sleep(ms: number): Promise<void> {
|
||||
return new Promise((resolve) => setTimeout(resolve, ms));
|
||||
return new Promise((resolve) => setTimeout(resolve, ms));
|
||||
}
|
||||
|
||||
/**
|
||||
* Retry a function with exponential backoff
|
||||
*/
|
||||
export async function retry<T>(
|
||||
fn: () => Promise<T>,
|
||||
options: {
|
||||
maxAttempts?: number;
|
||||
initialDelay?: number;
|
||||
maxDelay?: number;
|
||||
backoffMultiplier?: number;
|
||||
} = {}
|
||||
fn: () => Promise<T>,
|
||||
options: {
|
||||
maxAttempts?: number;
|
||||
initialDelay?: number;
|
||||
maxDelay?: number;
|
||||
backoffMultiplier?: number;
|
||||
} = {}
|
||||
): Promise<T> {
|
||||
const {
|
||||
maxAttempts = 3,
|
||||
initialDelay = 1000,
|
||||
maxDelay = 10000,
|
||||
backoffMultiplier = 2,
|
||||
} = options;
|
||||
const { maxAttempts = 3, initialDelay = 1000, maxDelay = 10000, backoffMultiplier = 2 } = options;
|
||||
|
||||
let lastError: Error | undefined;
|
||||
let delay = initialDelay;
|
||||
let lastError: Error | undefined;
|
||||
let delay = initialDelay;
|
||||
|
||||
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
||||
try {
|
||||
return await fn();
|
||||
} catch (error) {
|
||||
lastError = error as Error;
|
||||
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
||||
try {
|
||||
return await fn();
|
||||
} catch (error) {
|
||||
lastError = error as Error;
|
||||
|
||||
if (attempt === maxAttempts) {
|
||||
break;
|
||||
}
|
||||
if (attempt === maxAttempts) {
|
||||
break;
|
||||
}
|
||||
|
||||
await sleep(delay);
|
||||
delay = Math.min(delay * backoffMultiplier, maxDelay);
|
||||
}
|
||||
}
|
||||
await sleep(delay);
|
||||
delay = Math.min(delay * backoffMultiplier, maxDelay);
|
||||
}
|
||||
}
|
||||
|
||||
throw lastError;
|
||||
throw lastError;
|
||||
}
|
||||
|
||||
/**
|
||||
* Debounce a function
|
||||
*/
|
||||
export function debounce<T extends (...args: any[]) => any>(
|
||||
fn: T,
|
||||
delay: number
|
||||
fn: T,
|
||||
delay: number
|
||||
): (...args: Parameters<T>) => void {
|
||||
let timeoutId: NodeJS.Timeout | null = null;
|
||||
let timeoutId: NodeJS.Timeout | null = null;
|
||||
|
||||
return (...args: Parameters<T>) => {
|
||||
if (timeoutId) {
|
||||
clearTimeout(timeoutId);
|
||||
}
|
||||
return (...args: Parameters<T>) => {
|
||||
if (timeoutId) {
|
||||
clearTimeout(timeoutId);
|
||||
}
|
||||
|
||||
timeoutId = setTimeout(() => {
|
||||
fn(...args);
|
||||
timeoutId = null;
|
||||
}, delay);
|
||||
};
|
||||
timeoutId = setTimeout(() => {
|
||||
fn(...args);
|
||||
timeoutId = null;
|
||||
}, delay);
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -111,7 +111,7 @@ export function createCache<T = string>(config: CacheConfig) {
|
|||
id,
|
||||
data,
|
||||
expires: Date.now() + expiresInMs,
|
||||
createdAt: Date.now()
|
||||
createdAt: Date.now(),
|
||||
};
|
||||
|
||||
store.put(entry);
|
||||
|
|
@ -180,7 +180,7 @@ export function createCache<T = string>(config: CacheConfig) {
|
|||
async has(id: string): Promise<boolean> {
|
||||
const data = await this.get(id);
|
||||
return data !== null;
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -188,7 +188,7 @@ export function createCache<T = string>(config: CacheConfig) {
|
|||
const DEFAULT_URL_CACHE_CONFIG: CacheConfig = {
|
||||
dbName: 'manacore-cache',
|
||||
storeName: 'urls',
|
||||
version: 1
|
||||
version: 1,
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -6,8 +6,8 @@ import { format, formatDistanceToNow, parseISO, isToday, isYesterday } from 'dat
|
|||
import { de, enUS } from 'date-fns/locale';
|
||||
|
||||
const locales = {
|
||||
de,
|
||||
en: enUS,
|
||||
de,
|
||||
en: enUS,
|
||||
};
|
||||
|
||||
export type LocaleKey = keyof typeof locales;
|
||||
|
|
@ -16,30 +16,30 @@ export type LocaleKey = keyof typeof locales;
|
|||
* Format a date string to a readable format
|
||||
*/
|
||||
export function formatDate(
|
||||
date: string | Date,
|
||||
formatStr: string = 'PPP',
|
||||
locale: LocaleKey = 'de'
|
||||
date: string | Date,
|
||||
formatStr: string = 'PPP',
|
||||
locale: LocaleKey = 'de'
|
||||
): string {
|
||||
const dateObj = typeof date === 'string' ? parseISO(date) : date;
|
||||
return format(dateObj, formatStr, { locale: locales[locale] });
|
||||
const dateObj = typeof date === 'string' ? parseISO(date) : date;
|
||||
return format(dateObj, formatStr, { locale: locales[locale] });
|
||||
}
|
||||
|
||||
/**
|
||||
* Get relative time from now (e.g., "2 hours ago")
|
||||
*/
|
||||
export function formatRelativeTime(date: string | Date, locale: LocaleKey = 'de'): string {
|
||||
const dateObj = typeof date === 'string' ? parseISO(date) : date;
|
||||
return formatDistanceToNow(dateObj, {
|
||||
addSuffix: true,
|
||||
locale: locales[locale],
|
||||
});
|
||||
const dateObj = typeof date === 'string' ? parseISO(date) : date;
|
||||
return formatDistanceToNow(dateObj, {
|
||||
addSuffix: true,
|
||||
locale: locales[locale],
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Format a date for API requests (ISO string)
|
||||
*/
|
||||
export function toISOString(date: Date): string {
|
||||
return date.toISOString();
|
||||
return date.toISOString();
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -50,28 +50,25 @@ export function toISOString(date: Date): string {
|
|||
* - Yesterday → "Yesterday, 14:30" / "Gestern, 14:30"
|
||||
* - Other → "15. März 2024, 14:30" / "March 15, 2024, 2:30 PM"
|
||||
*/
|
||||
export function formatTimestamp(
|
||||
date: string | Date,
|
||||
locale: LocaleKey = 'de'
|
||||
): string {
|
||||
const dateObj = typeof date === 'string' ? parseISO(date) : date;
|
||||
const timeFormat = locale === 'de' ? 'HH:mm' : 'h:mm a';
|
||||
export function formatTimestamp(date: string | Date, locale: LocaleKey = 'de'): string {
|
||||
const dateObj = typeof date === 'string' ? parseISO(date) : date;
|
||||
const timeFormat = locale === 'de' ? 'HH:mm' : 'h:mm a';
|
||||
|
||||
const labels = {
|
||||
de: { today: 'Heute', yesterday: 'Gestern' },
|
||||
en: { today: 'Today', yesterday: 'Yesterday' },
|
||||
};
|
||||
const labels = {
|
||||
de: { today: 'Heute', yesterday: 'Gestern' },
|
||||
en: { today: 'Today', yesterday: 'Yesterday' },
|
||||
};
|
||||
|
||||
if (isToday(dateObj)) {
|
||||
return `${labels[locale].today}, ${format(dateObj, timeFormat)}`;
|
||||
}
|
||||
if (isToday(dateObj)) {
|
||||
return `${labels[locale].today}, ${format(dateObj, timeFormat)}`;
|
||||
}
|
||||
|
||||
if (isYesterday(dateObj)) {
|
||||
return `${labels[locale].yesterday}, ${format(dateObj, timeFormat)}`;
|
||||
}
|
||||
if (isYesterday(dateObj)) {
|
||||
return `${labels[locale].yesterday}, ${format(dateObj, timeFormat)}`;
|
||||
}
|
||||
|
||||
const dateFormat = locale === 'de' ? 'd. MMMM yyyy' : 'MMMM d, yyyy';
|
||||
return `${format(dateObj, dateFormat, { locale: locales[locale] })}, ${format(dateObj, timeFormat)}`;
|
||||
const dateFormat = locale === 'de' ? 'd. MMMM yyyy' : 'MMMM d, yyyy';
|
||||
return `${format(dateObj, dateFormat, { locale: locales[locale] })}, ${format(dateObj, timeFormat)}`;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -87,7 +84,7 @@ export { isToday, isYesterday } from 'date-fns';
|
|||
* @returns Formatted short date string (e.g., "15 Mar 2024" or "15. Mär. 2024")
|
||||
*/
|
||||
export function formatShortDate(date: Date | string, locale: LocaleKey = 'de'): string {
|
||||
const dateObj = typeof date === 'string' ? parseISO(date) : date;
|
||||
const dateObj = typeof date === 'string' ? parseISO(date) : date;
|
||||
|
||||
return format(dateObj, 'dd MMM yyyy', { locale: locales[locale] });
|
||||
return format(dateObj, 'dd MMM yyyy', { locale: locales[locale] });
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,149 +6,138 @@
|
|||
* Format duration from seconds to MM:SS or HH:MM:SS format
|
||||
*/
|
||||
export function formatDuration(seconds: number): string {
|
||||
if (!Number.isFinite(seconds) || seconds < 0) {
|
||||
return '--:--';
|
||||
}
|
||||
if (!Number.isFinite(seconds) || seconds < 0) {
|
||||
return '--:--';
|
||||
}
|
||||
|
||||
const hours = Math.floor(seconds / 3600);
|
||||
const minutes = Math.floor((seconds % 3600) / 60);
|
||||
const remainingSeconds = Math.floor(seconds % 60);
|
||||
const hours = Math.floor(seconds / 3600);
|
||||
const minutes = Math.floor((seconds % 3600) / 60);
|
||||
const remainingSeconds = Math.floor(seconds % 60);
|
||||
|
||||
if (hours > 0) {
|
||||
return `${hours}:${minutes.toString().padStart(2, '0')}:${remainingSeconds.toString().padStart(2, '0')}`;
|
||||
}
|
||||
if (hours > 0) {
|
||||
return `${hours}:${minutes.toString().padStart(2, '0')}:${remainingSeconds.toString().padStart(2, '0')}`;
|
||||
}
|
||||
|
||||
return `${minutes}:${remainingSeconds.toString().padStart(2, '0')}`;
|
||||
return `${minutes}:${remainingSeconds.toString().padStart(2, '0')}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Format duration from milliseconds
|
||||
*/
|
||||
export function formatDurationFromMs(milliseconds: number): string {
|
||||
return formatDuration(milliseconds / 1000);
|
||||
return formatDuration(milliseconds / 1000);
|
||||
}
|
||||
|
||||
/**
|
||||
* Format duration with units (e.g., "2 min 30 sec" or "1h 23m")
|
||||
*/
|
||||
export function formatDurationWithUnits(
|
||||
seconds: number,
|
||||
locale: 'en' | 'de' = 'en'
|
||||
): string {
|
||||
if (!Number.isFinite(seconds) || seconds < 0) {
|
||||
return locale === 'de' ? 'keine Zeit' : 'no time';
|
||||
}
|
||||
export function formatDurationWithUnits(seconds: number, locale: 'en' | 'de' = 'en'): string {
|
||||
if (!Number.isFinite(seconds) || seconds < 0) {
|
||||
return locale === 'de' ? 'keine Zeit' : 'no time';
|
||||
}
|
||||
|
||||
const hours = Math.floor(seconds / 3600);
|
||||
const minutes = Math.floor((seconds % 3600) / 60);
|
||||
const remainingSeconds = Math.floor(seconds % 60);
|
||||
const hours = Math.floor(seconds / 3600);
|
||||
const minutes = Math.floor((seconds % 3600) / 60);
|
||||
const remainingSeconds = Math.floor(seconds % 60);
|
||||
|
||||
if (hours > 0) {
|
||||
return minutes > 0 ? `${hours}h ${minutes}m` : `${hours}h`;
|
||||
}
|
||||
if (hours > 0) {
|
||||
return minutes > 0 ? `${hours}h ${minutes}m` : `${hours}h`;
|
||||
}
|
||||
|
||||
if (minutes > 0) {
|
||||
return remainingSeconds > 0 && minutes < 5
|
||||
? `${minutes}m ${remainingSeconds}s`
|
||||
: `${minutes}m`;
|
||||
}
|
||||
if (minutes > 0) {
|
||||
return remainingSeconds > 0 && minutes < 5 ? `${minutes}m ${remainingSeconds}s` : `${minutes}m`;
|
||||
}
|
||||
|
||||
return `${remainingSeconds}s`;
|
||||
return `${remainingSeconds}s`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Format duration to human readable text
|
||||
*/
|
||||
export function formatDurationHumanReadable(
|
||||
seconds: number,
|
||||
locale: 'en' | 'de' = 'de'
|
||||
): string {
|
||||
if (!Number.isFinite(seconds) || seconds < 0) {
|
||||
return locale === 'de' ? 'keine Zeit' : 'no time';
|
||||
}
|
||||
export function formatDurationHumanReadable(seconds: number, locale: 'en' | 'de' = 'de'): string {
|
||||
if (!Number.isFinite(seconds) || seconds < 0) {
|
||||
return locale === 'de' ? 'keine Zeit' : 'no time';
|
||||
}
|
||||
|
||||
const hours = Math.floor(seconds / 3600);
|
||||
const minutes = Math.floor((seconds % 3600) / 60);
|
||||
const remainingSeconds = Math.floor(seconds % 60);
|
||||
const hours = Math.floor(seconds / 3600);
|
||||
const minutes = Math.floor((seconds % 3600) / 60);
|
||||
const remainingSeconds = Math.floor(seconds % 60);
|
||||
|
||||
const parts: string[] = [];
|
||||
const parts: string[] = [];
|
||||
|
||||
if (locale === 'de') {
|
||||
if (hours > 0) {
|
||||
parts.push(`${hours} ${hours === 1 ? 'Stunde' : 'Stunden'}`);
|
||||
}
|
||||
if (minutes > 0) {
|
||||
parts.push(`${minutes} ${minutes === 1 ? 'Minute' : 'Minuten'}`);
|
||||
}
|
||||
if (remainingSeconds > 0 && hours === 0) {
|
||||
parts.push(`${remainingSeconds} ${remainingSeconds === 1 ? 'Sekunde' : 'Sekunden'}`);
|
||||
}
|
||||
return parts.length === 0 ? '0 Sekunden' : parts.join(' ');
|
||||
} else {
|
||||
if (hours > 0) {
|
||||
parts.push(`${hours} ${hours === 1 ? 'hour' : 'hours'}`);
|
||||
}
|
||||
if (minutes > 0) {
|
||||
parts.push(`${minutes} ${minutes === 1 ? 'minute' : 'minutes'}`);
|
||||
}
|
||||
if (remainingSeconds > 0 && hours === 0) {
|
||||
parts.push(`${remainingSeconds} ${remainingSeconds === 1 ? 'second' : 'seconds'}`);
|
||||
}
|
||||
return parts.length === 0 ? '0 seconds' : parts.join(' ');
|
||||
}
|
||||
if (locale === 'de') {
|
||||
if (hours > 0) {
|
||||
parts.push(`${hours} ${hours === 1 ? 'Stunde' : 'Stunden'}`);
|
||||
}
|
||||
if (minutes > 0) {
|
||||
parts.push(`${minutes} ${minutes === 1 ? 'Minute' : 'Minuten'}`);
|
||||
}
|
||||
if (remainingSeconds > 0 && hours === 0) {
|
||||
parts.push(`${remainingSeconds} ${remainingSeconds === 1 ? 'Sekunde' : 'Sekunden'}`);
|
||||
}
|
||||
return parts.length === 0 ? '0 Sekunden' : parts.join(' ');
|
||||
} else {
|
||||
if (hours > 0) {
|
||||
parts.push(`${hours} ${hours === 1 ? 'hour' : 'hours'}`);
|
||||
}
|
||||
if (minutes > 0) {
|
||||
parts.push(`${minutes} ${minutes === 1 ? 'minute' : 'minutes'}`);
|
||||
}
|
||||
if (remainingSeconds > 0 && hours === 0) {
|
||||
parts.push(`${remainingSeconds} ${remainingSeconds === 1 ? 'second' : 'seconds'}`);
|
||||
}
|
||||
return parts.length === 0 ? '0 seconds' : parts.join(' ');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Format file size from bytes to human readable string
|
||||
*/
|
||||
export function formatFileSize(bytes: number, decimals: number = 1): string {
|
||||
if (bytes === 0) return '0 B';
|
||||
if (!Number.isFinite(bytes) || bytes < 0) return '--';
|
||||
if (bytes === 0) return '0 B';
|
||||
if (!Number.isFinite(bytes) || bytes < 0) return '--';
|
||||
|
||||
const k = 1024;
|
||||
const sizes = ['B', 'KB', 'MB', 'GB', 'TB'];
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||
const k = 1024;
|
||||
const sizes = ['B', 'KB', 'MB', 'GB', 'TB'];
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||
|
||||
return `${parseFloat((bytes / Math.pow(k, i)).toFixed(decimals))} ${sizes[i]}`;
|
||||
return `${parseFloat((bytes / Math.pow(k, i)).toFixed(decimals))} ${sizes[i]}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Format number with thousands separator
|
||||
*/
|
||||
export function formatNumber(
|
||||
num: number,
|
||||
locale: string = 'de-DE'
|
||||
): string {
|
||||
return num.toLocaleString(locale);
|
||||
export function formatNumber(num: number, locale: string = 'de-DE'): string {
|
||||
return num.toLocaleString(locale);
|
||||
}
|
||||
|
||||
/**
|
||||
* Format currency
|
||||
*/
|
||||
export function formatCurrency(
|
||||
amount: number,
|
||||
currency: string = 'EUR',
|
||||
locale: string = 'de-DE'
|
||||
amount: number,
|
||||
currency: string = 'EUR',
|
||||
locale: string = 'de-DE'
|
||||
): string {
|
||||
return new Intl.NumberFormat(locale, {
|
||||
style: 'currency',
|
||||
currency,
|
||||
}).format(amount);
|
||||
return new Intl.NumberFormat(locale, {
|
||||
style: 'currency',
|
||||
currency,
|
||||
}).format(amount);
|
||||
}
|
||||
|
||||
/**
|
||||
* Format percentage
|
||||
*/
|
||||
export function formatPercent(
|
||||
value: number,
|
||||
decimals: number = 0,
|
||||
locale: string = 'de-DE'
|
||||
value: number,
|
||||
decimals: number = 0,
|
||||
locale: string = 'de-DE'
|
||||
): string {
|
||||
return new Intl.NumberFormat(locale, {
|
||||
style: 'percent',
|
||||
minimumFractionDigits: decimals,
|
||||
maximumFractionDigits: decimals,
|
||||
}).format(value);
|
||||
return new Intl.NumberFormat(locale, {
|
||||
style: 'percent',
|
||||
minimumFractionDigits: decimals,
|
||||
maximumFractionDigits: decimals,
|
||||
}).format(value);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -164,15 +153,15 @@ export const formatDurationCompact = formatDuration;
|
|||
* @returns Duration in seconds
|
||||
*/
|
||||
export function parseDuration(duration: string): number {
|
||||
const parts = duration.split(':').map(Number);
|
||||
const parts = duration.split(':').map(Number);
|
||||
|
||||
if (parts.length === 3) {
|
||||
// Hours:Minutes:Seconds
|
||||
return parts[0] * 3600 + parts[1] * 60 + parts[2];
|
||||
} else if (parts.length === 2) {
|
||||
// Minutes:Seconds
|
||||
return parts[0] * 60 + parts[1];
|
||||
}
|
||||
if (parts.length === 3) {
|
||||
// Hours:Minutes:Seconds
|
||||
return parts[0] * 3600 + parts[1] * 60 + parts[2];
|
||||
} else if (parts.length === 2) {
|
||||
// Minutes:Seconds
|
||||
return parts[0] * 60 + parts[1];
|
||||
}
|
||||
|
||||
return parts[0] || 0;
|
||||
return parts[0] || 0;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -24,7 +24,9 @@ export interface ShortcutGroup {
|
|||
*/
|
||||
export function matchesShortcut(event: KeyboardEvent, shortcut: ShortcutAction): boolean {
|
||||
const keyMatches = event.key.toLowerCase() === shortcut.key.toLowerCase();
|
||||
const ctrlMatches = shortcut.ctrl ? event.ctrlKey || event.metaKey : !event.ctrlKey && !event.metaKey;
|
||||
const ctrlMatches = shortcut.ctrl
|
||||
? event.ctrlKey || event.metaKey
|
||||
: !event.ctrlKey && !event.metaKey;
|
||||
const shiftMatches = shortcut.shift ? event.shiftKey : !event.shiftKey;
|
||||
const altMatches = shortcut.alt ? event.altKey : !event.altKey;
|
||||
|
||||
|
|
@ -75,11 +77,7 @@ export function createShortcutHandler(
|
|||
// Don't handle shortcuts if user is typing in an input (unless explicitly allowed)
|
||||
if (!options?.allowInInputs) {
|
||||
const target = event.target as HTMLElement;
|
||||
if (
|
||||
target.tagName === 'INPUT' ||
|
||||
target.tagName === 'TEXTAREA' ||
|
||||
target.isContentEditable
|
||||
) {
|
||||
if (target.tagName === 'INPUT' || target.tagName === 'TEXTAREA' || target.isContentEditable) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
|
@ -120,7 +118,7 @@ export function createShortcuts(actions: {
|
|||
key: 'f',
|
||||
ctrl: true,
|
||||
description: 'Search',
|
||||
action: actions.onSearch
|
||||
action: actions.onSearch,
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -129,7 +127,7 @@ export function createShortcuts(actions: {
|
|||
key: 's',
|
||||
ctrl: true,
|
||||
description: 'Save',
|
||||
action: actions.onSave
|
||||
action: actions.onSave,
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -138,7 +136,7 @@ export function createShortcuts(actions: {
|
|||
key: 'e',
|
||||
ctrl: true,
|
||||
description: 'Edit',
|
||||
action: actions.onEdit
|
||||
action: actions.onEdit,
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -146,7 +144,7 @@ export function createShortcuts(actions: {
|
|||
generalShortcuts.push({
|
||||
key: 'Escape',
|
||||
description: 'Cancel',
|
||||
action: actions.onCancel
|
||||
action: actions.onCancel,
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -155,7 +153,7 @@ export function createShortcuts(actions: {
|
|||
key: 'n',
|
||||
ctrl: true,
|
||||
description: 'New',
|
||||
action: actions.onNew
|
||||
action: actions.onNew,
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -164,7 +162,7 @@ export function createShortcuts(actions: {
|
|||
key: 'Delete',
|
||||
ctrl: true,
|
||||
description: 'Delete',
|
||||
action: actions.onDelete
|
||||
action: actions.onDelete,
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -174,7 +172,7 @@ export function createShortcuts(actions: {
|
|||
ctrl: true,
|
||||
shift: true,
|
||||
description: 'Copy',
|
||||
action: actions.onCopy
|
||||
action: actions.onCopy,
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -183,7 +181,7 @@ export function createShortcuts(actions: {
|
|||
key: 'z',
|
||||
ctrl: true,
|
||||
description: 'Undo',
|
||||
action: actions.onUndo
|
||||
action: actions.onUndo,
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -193,14 +191,14 @@ export function createShortcuts(actions: {
|
|||
ctrl: true,
|
||||
shift: true,
|
||||
description: 'Redo',
|
||||
action: actions.onRedo
|
||||
action: actions.onRedo,
|
||||
});
|
||||
}
|
||||
|
||||
if (generalShortcuts.length > 0) {
|
||||
shortcuts.push({
|
||||
name: 'General',
|
||||
shortcuts: generalShortcuts
|
||||
shortcuts: generalShortcuts,
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -224,7 +222,7 @@ export function shortcuts(node: HTMLElement, shortcutActions: ShortcutAction[])
|
|||
node.removeEventListener('keydown', handler);
|
||||
const newHandler = createShortcutHandler(newShortcutActions);
|
||||
node.addEventListener('keydown', newHandler);
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -6,38 +6,38 @@
|
|||
* Truncate a string to a maximum length with ellipsis
|
||||
*/
|
||||
export function truncate(str: string, maxLength: number): string {
|
||||
if (str.length <= maxLength) return str;
|
||||
return str.slice(0, maxLength - 3) + '...';
|
||||
if (str.length <= maxLength) return str;
|
||||
return str.slice(0, maxLength - 3) + '...';
|
||||
}
|
||||
|
||||
/**
|
||||
* Capitalize the first letter of a string
|
||||
*/
|
||||
export function capitalize(str: string): string {
|
||||
if (!str) return str;
|
||||
return str.charAt(0).toUpperCase() + str.slice(1);
|
||||
if (!str) return str;
|
||||
return str.charAt(0).toUpperCase() + str.slice(1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a random string ID
|
||||
*/
|
||||
export function generateId(length: number = 8): string {
|
||||
const chars = 'abcdefghijklmnopqrstuvwxyz0123456789';
|
||||
let result = '';
|
||||
for (let i = 0; i < length; i++) {
|
||||
result += chars.charAt(Math.floor(Math.random() * chars.length));
|
||||
}
|
||||
return result;
|
||||
const chars = 'abcdefghijklmnopqrstuvwxyz0123456789';
|
||||
let result = '';
|
||||
for (let i = 0; i < length; i++) {
|
||||
result += chars.charAt(Math.floor(Math.random() * chars.length));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Slugify a string for URLs
|
||||
*/
|
||||
export function slugify(str: string): string {
|
||||
return str
|
||||
.toLowerCase()
|
||||
.trim()
|
||||
.replace(/[^\w\s-]/g, '')
|
||||
.replace(/[\s_-]+/g, '-')
|
||||
.replace(/^-+|-+$/g, '');
|
||||
return str
|
||||
.toLowerCase()
|
||||
.trim()
|
||||
.replace(/[^\w\s-]/g, '')
|
||||
.replace(/[\s_-]+/g, '-')
|
||||
.replace(/^-+|-+$/g, '');
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,29 +6,29 @@
|
|||
* Validate email address format
|
||||
*/
|
||||
export function isValidEmail(email: string): boolean {
|
||||
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
||||
return emailRegex.test(email);
|
||||
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
||||
return emailRegex.test(email);
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate URL format
|
||||
*/
|
||||
export function isValidUrl(url: string): boolean {
|
||||
try {
|
||||
new URL(url);
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
new URL(url);
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate phone number (basic international format)
|
||||
*/
|
||||
export function isValidPhone(phone: string): boolean {
|
||||
// Allows: +49123456789, 0123456789, +1 (555) 123-4567
|
||||
const phoneRegex = /^[+]?[(]?[0-9]{1,4}[)]?[-\s./0-9]*$/;
|
||||
return phoneRegex.test(phone) && phone.replace(/\D/g, '').length >= 6;
|
||||
// Allows: +49123456789, 0123456789, +1 (555) 123-4567
|
||||
const phoneRegex = /^[+]?[(]?[0-9]{1,4}[)]?[-\s./0-9]*$/;
|
||||
return phoneRegex.test(phone) && phone.replace(/\D/g, '').length >= 6;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -36,67 +36,68 @@ export function isValidPhone(phone: string): boolean {
|
|||
* Returns an object with validation details
|
||||
*/
|
||||
export function validatePassword(password: string): {
|
||||
isValid: boolean;
|
||||
hasMinLength: boolean;
|
||||
hasUppercase: boolean;
|
||||
hasLowercase: boolean;
|
||||
hasNumber: boolean;
|
||||
hasSpecialChar: boolean;
|
||||
score: number;
|
||||
isValid: boolean;
|
||||
hasMinLength: boolean;
|
||||
hasUppercase: boolean;
|
||||
hasLowercase: boolean;
|
||||
hasNumber: boolean;
|
||||
hasSpecialChar: boolean;
|
||||
score: number;
|
||||
} {
|
||||
const hasMinLength = password.length >= 8;
|
||||
const hasUppercase = /[A-Z]/.test(password);
|
||||
const hasLowercase = /[a-z]/.test(password);
|
||||
const hasNumber = /[0-9]/.test(password);
|
||||
const hasSpecialChar = /[!@#$%^&*(),.?":{}|<>]/.test(password);
|
||||
const hasMinLength = password.length >= 8;
|
||||
const hasUppercase = /[A-Z]/.test(password);
|
||||
const hasLowercase = /[a-z]/.test(password);
|
||||
const hasNumber = /[0-9]/.test(password);
|
||||
const hasSpecialChar = /[!@#$%^&*(),.?":{}|<>]/.test(password);
|
||||
|
||||
const score = [hasMinLength, hasUppercase, hasLowercase, hasNumber, hasSpecialChar]
|
||||
.filter(Boolean).length;
|
||||
const score = [hasMinLength, hasUppercase, hasLowercase, hasNumber, hasSpecialChar].filter(
|
||||
Boolean
|
||||
).length;
|
||||
|
||||
return {
|
||||
isValid: hasMinLength && hasUppercase && hasLowercase && hasNumber,
|
||||
hasMinLength,
|
||||
hasUppercase,
|
||||
hasLowercase,
|
||||
hasNumber,
|
||||
hasSpecialChar,
|
||||
score,
|
||||
};
|
||||
return {
|
||||
isValid: hasMinLength && hasUppercase && hasLowercase && hasNumber,
|
||||
hasMinLength,
|
||||
hasUppercase,
|
||||
hasLowercase,
|
||||
hasNumber,
|
||||
hasSpecialChar,
|
||||
score,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a string is empty or only whitespace
|
||||
*/
|
||||
export function isEmpty(value: string | null | undefined): boolean {
|
||||
return !value || value.trim().length === 0;
|
||||
return !value || value.trim().length === 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a string exceeds max length
|
||||
*/
|
||||
export function isWithinMaxLength(value: string, maxLength: number): boolean {
|
||||
return value.length <= maxLength;
|
||||
return value.length <= maxLength;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a string meets minimum length
|
||||
*/
|
||||
export function meetsMinLength(value: string, minLength: number): boolean {
|
||||
return value.length >= minLength;
|
||||
return value.length >= minLength;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate UUID format
|
||||
*/
|
||||
export function isValidUuid(uuid: string): boolean {
|
||||
const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
|
||||
return uuidRegex.test(uuid);
|
||||
const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
|
||||
return uuidRegex.test(uuid);
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate hex color format
|
||||
*/
|
||||
export function isValidHexColor(color: string): boolean {
|
||||
const hexRegex = /^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$/;
|
||||
return hexRegex.test(color);
|
||||
const hexRegex = /^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$/;
|
||||
return hexRegex.test(color);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,18 +1,18 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "bundler",
|
||||
"lib": ["ES2022", "DOM"],
|
||||
"types": ["node"],
|
||||
"strict": true,
|
||||
"esModuleInterop": true,
|
||||
"skipLibCheck": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"isolatedModules": true,
|
||||
"verbatimModuleSyntax": true,
|
||||
"noEmit": true
|
||||
},
|
||||
"include": ["src/**/*"],
|
||||
"exclude": ["node_modules"]
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "bundler",
|
||||
"lib": ["ES2022", "DOM"],
|
||||
"types": ["node"],
|
||||
"strict": true,
|
||||
"esModuleInterop": true,
|
||||
"skipLibCheck": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"isolatedModules": true,
|
||||
"verbatimModuleSyntax": true,
|
||||
"noEmit": true
|
||||
},
|
||||
"include": ["src/**/*"],
|
||||
"exclude": ["node_modules"]
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue