mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-18 06:29:40 +02:00
Commit Message feat: implement comprehensive shared packages architecture for monorepo SUMMARY: Introduce 10 shared packages to unify common code across all 4 web apps, reducing ~3,000 lines of duplicated code and establishing consistent patterns for authentication, UI components, theming, and utilities. NEW SHARED PACKAGES: - @manacore/shared-auth: Unified auth logic (token management, JWT utils, fetch interceptor, storage/device/network adapters) - @manacore/shared-auth-ui: Reusable auth UI (LoginPage, RegisterPage, OAuth buttons for Google/Apple) - @manacore/shared-tailwind: Unified Tailwind config with 4 themes (lume, nature, stone, ocean) and light/dark mode support - @manacore/shared-icons: Phosphor-based icon library (40+ icons) - @manacore/shared-ui: Atomic design system (Text, Button, Badge, Toggle, Input, Modal) - @manacore/shared-i18n: Unified i18n setup with locale detection - @manacore/shared-config: Environment validation with Zod - @manacore/shared-subscriptio n-types: Subscription type definitions - @manacore/shared-subscriptio n-ui: Subscription UI components (planned) EXTENDED PACKAGES: - @manacore/shared-types: Added auth.ts, theme.ts, ui.ts, common.ts - @manacore/shared-utils: Added format.ts, validation.ts APP MIGRATIONS: - memoro/web: Migrated login (549→46 LOC), tailwind (165→12 LOC), removed 15+ duplicate components - manacore/web: Migrated to client-side auth with shared-auth, added new components (Icon, ThemeToggle, Logo) - manadeck/web: Replaced local authService/tokenManager with shared-auth, migrated auth pages - maerchenzauber/web: Added auth setup, stores, components, routes DELETED FILES (migrated to shared packages): - OAuth buttons (Google/Apple) from memoro, manacore, manadeck - Local authService, tokenManager, deviceManager, jwt utils - Duplicate Modal, Toggle, Text components - iconPaths and ManaIcon components - Subscription-related components (CostCard, PackageCard, etc.) BENEFITS: - 92% reduction in login page code - 93% reduction in tailwind config code - Consistent theming across all apps - Single source of truth for auth logic - Easier maintenance and updates BREAKING CHANGES: - Icon imports now from @manacore/shared-icons - Modal imports from @manacore/shared-ui - OAuth config via setGoogleCl ientId()/setAppleConfig()
This commit is contained in:
parent
725db638ea
commit
ef70a1af0b
198 changed files with 11113 additions and 3656 deletions
152
packages/shared-utils/src/format.ts
Normal file
152
packages/shared-utils/src/format.ts
Normal file
|
|
@ -0,0 +1,152 @@
|
|||
/**
|
||||
* Formatting utility functions
|
||||
*/
|
||||
|
||||
/**
|
||||
* 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 '--:--';
|
||||
}
|
||||
|
||||
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')}`;
|
||||
}
|
||||
|
||||
return `${minutes}:${remainingSeconds.toString().padStart(2, '0')}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Format duration from milliseconds
|
||||
*/
|
||||
export function formatDurationFromMs(milliseconds: number): string {
|
||||
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';
|
||||
}
|
||||
|
||||
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 (minutes > 0) {
|
||||
return remainingSeconds > 0 && minutes < 5
|
||||
? `${minutes}m ${remainingSeconds}s`
|
||||
: `${minutes}m`;
|
||||
}
|
||||
|
||||
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';
|
||||
}
|
||||
|
||||
const hours = Math.floor(seconds / 3600);
|
||||
const minutes = Math.floor((seconds % 3600) / 60);
|
||||
const remainingSeconds = Math.floor(seconds % 60);
|
||||
|
||||
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(' ');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 '--';
|
||||
|
||||
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]}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Format number with thousands separator
|
||||
*/
|
||||
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'
|
||||
): string {
|
||||
return new Intl.NumberFormat(locale, {
|
||||
style: 'currency',
|
||||
currency,
|
||||
}).format(amount);
|
||||
}
|
||||
|
||||
/**
|
||||
* Format percentage
|
||||
*/
|
||||
export function formatPercent(
|
||||
value: number,
|
||||
decimals: number = 0,
|
||||
locale: string = 'de-DE'
|
||||
): string {
|
||||
return new Intl.NumberFormat(locale, {
|
||||
style: 'percent',
|
||||
minimumFractionDigits: decimals,
|
||||
maximumFractionDigits: decimals,
|
||||
}).format(value);
|
||||
}
|
||||
|
|
@ -10,3 +10,9 @@ export * from './string';
|
|||
|
||||
// Async utilities
|
||||
export * from './async';
|
||||
|
||||
// Format utilities
|
||||
export * from './format';
|
||||
|
||||
// Validation utilities
|
||||
export * from './validation';
|
||||
|
|
|
|||
102
packages/shared-utils/src/validation.ts
Normal file
102
packages/shared-utils/src/validation.ts
Normal file
|
|
@ -0,0 +1,102 @@
|
|||
/**
|
||||
* Validation utility functions
|
||||
*/
|
||||
|
||||
/**
|
||||
* Validate email address format
|
||||
*/
|
||||
export function isValidEmail(email: string): boolean {
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate password strength
|
||||
* 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;
|
||||
} {
|
||||
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;
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a string exceeds max length
|
||||
*/
|
||||
export function isWithinMaxLength(value: string, maxLength: number): boolean {
|
||||
return value.length <= maxLength;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a string meets minimum length
|
||||
*/
|
||||
export function meetsMinLength(value: string, minLength: number): boolean {
|
||||
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);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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);
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue