managarten/packages/shared-config/src/api.ts
Till-JS ef70a1af0b 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()
2025-11-24 21:09:20 +01:00

207 lines
4.4 KiB
TypeScript

/**
* API endpoint construction utilities
*/
/**
* API configuration
*/
export interface ApiConfig {
/** Base URL for the API */
baseUrl: string;
/** API version prefix (e.g., 'v1') */
version?: string;
/** Default timeout in milliseconds */
timeout?: number;
/** Default headers */
headers?: Record<string, string>;
}
/**
* Create API endpoint URL builder
*/
export function createApiBuilder(config: ApiConfig) {
const { baseUrl, version } = config;
// Remove trailing slash from base URL
const base = baseUrl.replace(/\/$/, '');
// Build base path with optional version
const basePath = version ? `${base}/${version}` : base;
return {
/**
* Build endpoint URL from path segments
*/
endpoint(...segments: (string | number)[]): string {
const path = segments
.map(String)
.map(s => s.replace(/^\/+|\/+$/g, '')) // Remove leading/trailing slashes
.filter(Boolean)
.join('/');
return `${basePath}/${path}`;
},
/**
* Build endpoint URL with query parameters
*/
endpointWithQuery(
path: string | string[],
params?: Record<string, string | number | boolean | undefined>
): string {
const segments = Array.isArray(path) ? path : [path];
const url = this.endpoint(...segments);
if (!params) {
return url;
}
const searchParams = new URLSearchParams();
for (const [key, value] of Object.entries(params)) {
if (value !== undefined) {
searchParams.append(key, String(value));
}
}
const queryString = searchParams.toString();
return queryString ? `${url}?${queryString}` : url;
},
/**
* Get the base URL
*/
getBaseUrl(): string {
return basePath;
},
/**
* Get the config
*/
getConfig(): ApiConfig {
return config;
},
};
}
/**
* Build URL with query parameters
*/
export function buildUrl(
baseUrl: string,
path: string,
params?: Record<string, string | number | boolean | undefined>
): string {
// Ensure single slash between base and path
const base = baseUrl.replace(/\/$/, '');
const cleanPath = path.replace(/^\//, '');
const url = `${base}/${cleanPath}`;
if (!params) {
return url;
}
const searchParams = new URLSearchParams();
for (const [key, value] of Object.entries(params)) {
if (value !== undefined) {
searchParams.append(key, String(value));
}
}
const queryString = searchParams.toString();
return queryString ? `${url}?${queryString}` : url;
}
/**
* Parse URL and extract components
*/
export function parseUrl(url: string): {
protocol: string;
host: string;
port: string;
pathname: string;
search: string;
params: Record<string, string>;
} {
const urlObj = new URL(url);
const params: Record<string, string> = {};
urlObj.searchParams.forEach((value, key) => {
params[key] = value;
});
return {
protocol: urlObj.protocol.replace(':', ''),
host: urlObj.hostname,
port: urlObj.port,
pathname: urlObj.pathname,
search: urlObj.search,
params,
};
}
/**
* Join URL path segments
*/
export function joinPath(...segments: string[]): string {
return segments
.map(s => s.replace(/^\/+|\/+$/g, ''))
.filter(Boolean)
.join('/');
}
/**
* Common HTTP methods
*/
export const HTTP_METHODS = {
GET: 'GET',
POST: 'POST',
PUT: 'PUT',
PATCH: 'PATCH',
DELETE: 'DELETE',
HEAD: 'HEAD',
OPTIONS: 'OPTIONS',
} as const;
export type HttpMethod = typeof HTTP_METHODS[keyof typeof HTTP_METHODS];
/**
* Common HTTP status codes
*/
export const HTTP_STATUS = {
OK: 200,
CREATED: 201,
NO_CONTENT: 204,
BAD_REQUEST: 400,
UNAUTHORIZED: 401,
FORBIDDEN: 403,
NOT_FOUND: 404,
CONFLICT: 409,
UNPROCESSABLE_ENTITY: 422,
TOO_MANY_REQUESTS: 429,
INTERNAL_SERVER_ERROR: 500,
BAD_GATEWAY: 502,
SERVICE_UNAVAILABLE: 503,
} as const;
export type HttpStatus = typeof HTTP_STATUS[keyof typeof HTTP_STATUS];
/**
* Check if status code is successful (2xx)
*/
export function isSuccessStatus(status: number): boolean {
return status >= 200 && status < 300;
}
/**
* Check if status code is client error (4xx)
*/
export function isClientError(status: number): boolean {
return status >= 400 && status < 500;
}
/**
* Check if status code is server error (5xx)
*/
export function isServerError(status: number): boolean {
return status >= 500 && status < 600;
}