mirror of
https://github.com/aljazceru/enclava.git
synced 2025-12-17 07:24:34 +01:00
lib
This commit is contained in:
97
frontend/src/lib/api-client.ts
Normal file
97
frontend/src/lib/api-client.ts
Normal file
@@ -0,0 +1,97 @@
|
||||
import axios from 'axios';
|
||||
import Cookies from 'js-cookie';
|
||||
|
||||
const API_BASE_URL = process.env.NEXT_PUBLIC_BASE_URL || '';
|
||||
|
||||
const axiosInstance = axios.create({
|
||||
baseURL: API_BASE_URL,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
});
|
||||
|
||||
// Request interceptor to add auth token
|
||||
axiosInstance.interceptors.request.use(
|
||||
(config) => {
|
||||
const token = Cookies.get('access_token');
|
||||
if (token) {
|
||||
config.headers.Authorization = `Bearer ${token}`;
|
||||
}
|
||||
return config;
|
||||
},
|
||||
(error) => {
|
||||
return Promise.reject(error);
|
||||
}
|
||||
);
|
||||
|
||||
// Response interceptor to handle token refresh
|
||||
axiosInstance.interceptors.response.use(
|
||||
(response) => response,
|
||||
async (error) => {
|
||||
const originalRequest = error.config;
|
||||
|
||||
if (error.response?.status === 401 && !originalRequest._retry) {
|
||||
originalRequest._retry = true;
|
||||
|
||||
try {
|
||||
const refreshToken = Cookies.get('refresh_token');
|
||||
if (refreshToken) {
|
||||
const response = await axios.post(`${API_BASE_URL}/api/auth/refresh`, {
|
||||
refresh_token: refreshToken,
|
||||
});
|
||||
|
||||
const { access_token } = response.data;
|
||||
Cookies.set('access_token', access_token, { expires: 7 });
|
||||
|
||||
originalRequest.headers.Authorization = `Bearer ${access_token}`;
|
||||
return axiosInstance(originalRequest);
|
||||
}
|
||||
} catch (refreshError) {
|
||||
// Refresh failed, redirect to login
|
||||
Cookies.remove('access_token');
|
||||
Cookies.remove('refresh_token');
|
||||
window.location.href = '/login';
|
||||
return Promise.reject(refreshError);
|
||||
}
|
||||
}
|
||||
|
||||
return Promise.reject(error);
|
||||
}
|
||||
);
|
||||
|
||||
export const apiClient = {
|
||||
get: async <T = any>(url: string, config?: any): Promise<T> => {
|
||||
const response = await axiosInstance.get(url, config);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
post: async <T = any>(url: string, data?: any, config?: any): Promise<T> => {
|
||||
const response = await axiosInstance.post(url, data, config);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
put: async <T = any>(url: string, data?: any, config?: any): Promise<T> => {
|
||||
const response = await axiosInstance.put(url, data, config);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
delete: async <T = any>(url: string, config?: any): Promise<T> => {
|
||||
const response = await axiosInstance.delete(url, config);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
patch: async <T = any>(url: string, data?: any, config?: any): Promise<T> => {
|
||||
const response = await axiosInstance.patch(url, data, config);
|
||||
return response.data;
|
||||
},
|
||||
};
|
||||
|
||||
// Chatbot specific API methods
|
||||
export const chatbotApi = {
|
||||
create: async (data: any) => apiClient.post('/api/chatbot/create', data),
|
||||
list: async () => apiClient.get('/api/chatbot/list'),
|
||||
update: async (id: string, data: any) => apiClient.put(`/api/chatbot/update/${id}`, data),
|
||||
delete: async (id: string) => apiClient.delete(`/api/chatbot/delete/${id}`),
|
||||
chat: async (id: string, message: string, config?: any) =>
|
||||
apiClient.post(`/api/chatbot/chat`, { chatbot_id: id, message, ...config }),
|
||||
};
|
||||
49
frontend/src/lib/config.ts
Normal file
49
frontend/src/lib/config.ts
Normal file
@@ -0,0 +1,49 @@
|
||||
export const config = {
|
||||
API_BASE_URL: process.env.NEXT_PUBLIC_BASE_URL || '',
|
||||
APP_NAME: process.env.NEXT_PUBLIC_APP_NAME || 'Enclava',
|
||||
DEFAULT_LANGUAGE: 'en',
|
||||
SUPPORTED_LANGUAGES: ['en', 'es', 'fr', 'de', 'it'],
|
||||
|
||||
// Feature flags
|
||||
FEATURES: {
|
||||
RAG: true,
|
||||
PLUGINS: true,
|
||||
ANALYTICS: true,
|
||||
AUDIT_LOGS: true,
|
||||
BUDGET_MANAGEMENT: true,
|
||||
},
|
||||
|
||||
// Default values
|
||||
DEFAULTS: {
|
||||
TEMPERATURE: 0.7,
|
||||
MAX_TOKENS: 1000,
|
||||
TOP_K: 5,
|
||||
MEMORY_LENGTH: 10,
|
||||
},
|
||||
|
||||
// API endpoints
|
||||
ENDPOINTS: {
|
||||
AUTH: {
|
||||
LOGIN: '/api/auth/login',
|
||||
REGISTER: '/api/auth/register',
|
||||
REFRESH: '/api/auth/refresh',
|
||||
ME: '/api/auth/me',
|
||||
},
|
||||
CHATBOT: {
|
||||
LIST: '/api/chatbot/list',
|
||||
CREATE: '/api/chatbot/create',
|
||||
UPDATE: '/api/chatbot/update/:id',
|
||||
DELETE: '/api/chatbot/delete/:id',
|
||||
CHAT: '/api/chatbot/chat',
|
||||
},
|
||||
LLM: {
|
||||
MODELS: '/api/llm/models',
|
||||
API_KEYS: '/api/llm/api-keys',
|
||||
BUDGETS: '/api/llm/budgets',
|
||||
},
|
||||
RAG: {
|
||||
COLLECTIONS: '/api/rag/collections',
|
||||
DOCUMENTS: '/api/rag/documents',
|
||||
},
|
||||
},
|
||||
};
|
||||
119
frontend/src/lib/file-download.ts
Normal file
119
frontend/src/lib/file-download.ts
Normal file
@@ -0,0 +1,119 @@
|
||||
export const downloadFile = async (url: string, filename?: string): Promise<void> => {
|
||||
try {
|
||||
const response = await fetch(url);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
|
||||
// Get the filename from the response headers if not provided
|
||||
const contentDisposition = response.headers.get('Content-Disposition');
|
||||
let defaultFilename = filename || 'download';
|
||||
|
||||
if (contentDisposition) {
|
||||
const filenameMatch = contentDisposition.match(/filename[^;=\n]*=((['"]).*?\2|[^;\n]*)/);
|
||||
if (filenameMatch && filenameMatch[1]) {
|
||||
defaultFilename = filenameMatch[1].replace(/['"]/g, '');
|
||||
}
|
||||
}
|
||||
|
||||
// Get the blob from the response
|
||||
const blob = await response.blob();
|
||||
|
||||
// Create a temporary URL for the blob
|
||||
const blobUrl = window.URL.createObjectURL(blob);
|
||||
|
||||
// Create a temporary link element
|
||||
const link = document.createElement('a');
|
||||
link.href = blobUrl;
|
||||
link.download = defaultFilename;
|
||||
|
||||
// Append the link to the body
|
||||
document.body.appendChild(link);
|
||||
|
||||
// Trigger the download
|
||||
link.click();
|
||||
|
||||
// Clean up
|
||||
document.body.removeChild(link);
|
||||
window.URL.revokeObjectURL(blobUrl);
|
||||
} catch (error) {
|
||||
console.error('Error downloading file:', error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
export const downloadFileFromData = (
|
||||
data: Blob | string,
|
||||
filename: string,
|
||||
mimeType?: string
|
||||
): void => {
|
||||
try {
|
||||
let blob: Blob;
|
||||
|
||||
if (typeof data === 'string') {
|
||||
blob = new Blob([data], { type: mimeType || 'text/plain' });
|
||||
} else {
|
||||
blob = data;
|
||||
}
|
||||
|
||||
const blobUrl = window.URL.createObjectURL(blob);
|
||||
const link = document.createElement('a');
|
||||
link.href = blobUrl;
|
||||
link.download = filename;
|
||||
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
|
||||
document.body.removeChild(link);
|
||||
window.URL.revokeObjectURL(blobUrl);
|
||||
} catch (error) {
|
||||
console.error('Error downloading file from data:', error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
export const uploadFile = async (
|
||||
file: File,
|
||||
url: string,
|
||||
onProgress?: (progress: number) => void
|
||||
): Promise<any> => {
|
||||
try {
|
||||
const formData = new FormData();
|
||||
formData.append('file', file);
|
||||
|
||||
const xhr = new XMLHttpRequest();
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
xhr.upload.onprogress = (event) => {
|
||||
if (event.lengthComputable && onProgress) {
|
||||
const progress = (event.loaded / event.total) * 100;
|
||||
onProgress(progress);
|
||||
}
|
||||
};
|
||||
|
||||
xhr.onload = () => {
|
||||
if (xhr.status >= 200 && xhr.status < 300) {
|
||||
try {
|
||||
const response = JSON.parse(xhr.responseText);
|
||||
resolve(response);
|
||||
} catch (error) {
|
||||
resolve(xhr.responseText);
|
||||
}
|
||||
} else {
|
||||
reject(new Error(`Upload failed with status ${xhr.status}`));
|
||||
}
|
||||
};
|
||||
|
||||
xhr.onerror = () => {
|
||||
reject(new Error('Network error during upload'));
|
||||
};
|
||||
|
||||
xhr.open('POST', url, true);
|
||||
xhr.send(formData);
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error uploading file:', error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
36
frontend/src/lib/id-utils.ts
Normal file
36
frontend/src/lib/id-utils.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
export function generateId(): string {
|
||||
return Math.random().toString(36).substr(2, 9);
|
||||
}
|
||||
|
||||
export function generateUniqueId(): string {
|
||||
return Date.now().toString(36) + Math.random().toString(36).substr(2);
|
||||
}
|
||||
|
||||
export function generateMessageId(): string {
|
||||
return `msg_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
|
||||
}
|
||||
|
||||
export function generateChatId(): string {
|
||||
return `chat_${Date.now()}_${Math.random().toString(36).substr(2, 5)}`;
|
||||
}
|
||||
|
||||
export function generateSessionId(): string {
|
||||
return `sess_${Date.now()}_${Math.random().toString(36).substr(2, 8)}`;
|
||||
}
|
||||
|
||||
export function generateShortId(): string {
|
||||
return Math.random().toString(36).substr(2, 6);
|
||||
}
|
||||
|
||||
export function generateTimestampId(): string {
|
||||
return `ts_${Date.now()}_${Math.random().toString(36).substr(2, 6)}`;
|
||||
}
|
||||
|
||||
export function isValidId(id: string): boolean {
|
||||
return typeof id === 'string' && id.length > 0;
|
||||
}
|
||||
|
||||
export function extractIdFromUrl(url: string): string | null {
|
||||
const match = url.match(/\/([a-f0-9]{8}-[a-f0-9]{4}-4[a-f0-9]{3}-[89ab][a-f0-9]{3}-[a-f0-9]{12})$/);
|
||||
return match ? match[1] : null;
|
||||
}
|
||||
176
frontend/src/lib/proxy-auth.ts
Normal file
176
frontend/src/lib/proxy-auth.ts
Normal file
@@ -0,0 +1,176 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
|
||||
// This is a proxy auth utility for server-side API routes
|
||||
// It handles authentication and proxying requests to the backend
|
||||
|
||||
export interface ProxyAuthConfig {
|
||||
backendUrl: string;
|
||||
requireAuth?: boolean;
|
||||
allowedRoles?: string[];
|
||||
}
|
||||
|
||||
export class ProxyAuth {
|
||||
private config: ProxyAuthConfig;
|
||||
|
||||
constructor(config: ProxyAuthConfig) {
|
||||
this.config = {
|
||||
requireAuth: true,
|
||||
...config,
|
||||
};
|
||||
}
|
||||
|
||||
async authenticate(request: NextRequest): Promise<{ success: boolean; user?: any; error?: string }> {
|
||||
// For server-side auth, we would typically validate the token
|
||||
// This is a simplified implementation
|
||||
const authHeader = request.headers.get('authorization');
|
||||
|
||||
if (!authHeader || !authHeader.startsWith('Bearer ')) {
|
||||
return { success: false, error: 'Missing or invalid authorization header' };
|
||||
}
|
||||
|
||||
const token = authHeader.substring(7);
|
||||
|
||||
// Here you would validate the token with your auth service
|
||||
// For now, we'll just check if it exists
|
||||
if (!token) {
|
||||
return { success: false, error: 'Invalid token' };
|
||||
}
|
||||
|
||||
// In a real implementation, you would decode and validate the JWT
|
||||
// and check user roles if required
|
||||
return {
|
||||
success: true,
|
||||
user: {
|
||||
id: 'user-id',
|
||||
email: 'user@example.com',
|
||||
role: 'user'
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
async proxyRequest(
|
||||
request: NextRequest,
|
||||
path: string,
|
||||
options?: {
|
||||
method?: string;
|
||||
headers?: Record<string, string>;
|
||||
body?: any;
|
||||
}
|
||||
): Promise<NextResponse> {
|
||||
// Authenticate the request if required
|
||||
if (this.config.requireAuth) {
|
||||
const authResult = await this.authenticate(request);
|
||||
if (!authResult.success) {
|
||||
return NextResponse.json(
|
||||
{ error: authResult.error || 'Authentication failed' },
|
||||
{ status: 401 }
|
||||
);
|
||||
}
|
||||
|
||||
// Check roles if specified
|
||||
if (this.config.allowedRoles && authResult.user) {
|
||||
if (!this.config.allowedRoles.includes(authResult.user.role)) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Insufficient permissions' },
|
||||
{ status: 403 }
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Build the target URL
|
||||
const targetUrl = new URL(path, this.config.backendUrl);
|
||||
|
||||
// Copy query parameters
|
||||
targetUrl.search = request.nextUrl.search;
|
||||
|
||||
// Prepare headers
|
||||
const headers: Record<string, string> = {
|
||||
'Content-Type': 'application/json',
|
||||
...options?.headers,
|
||||
};
|
||||
|
||||
// Forward authorization header if present
|
||||
const authHeader = request.headers.get('authorization');
|
||||
if (authHeader) {
|
||||
headers.authorization = authHeader;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(targetUrl, {
|
||||
method: options?.method || request.method,
|
||||
headers,
|
||||
body: options?.body ? JSON.stringify(options.body) : request.body,
|
||||
});
|
||||
|
||||
// Create a new response with the data
|
||||
const data = await response.json();
|
||||
|
||||
return NextResponse.json(data, {
|
||||
status: response.status,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Proxy request failed:', error);
|
||||
return NextResponse.json(
|
||||
{ error: 'Internal server error' },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Utility function to create a proxy handler
|
||||
export function createProxyHandler(config: ProxyAuthConfig) {
|
||||
const proxyAuth = new ProxyAuth(config);
|
||||
|
||||
return async (request: NextRequest, { params }: { params: { path?: string[] } }) => {
|
||||
const path = params?.path ? params.path.join('/') : '';
|
||||
return proxyAuth.proxyRequest(request, path);
|
||||
};
|
||||
}
|
||||
|
||||
// Simplified proxy request function for direct usage
|
||||
export async function proxyRequest(
|
||||
request: NextRequest,
|
||||
backendUrl: string,
|
||||
path: string = '',
|
||||
options?: {
|
||||
method?: string;
|
||||
headers?: Record<string, string>;
|
||||
body?: any;
|
||||
requireAuth?: boolean;
|
||||
}
|
||||
): Promise<NextResponse> {
|
||||
const proxyAuth = new ProxyAuth({
|
||||
backendUrl,
|
||||
requireAuth: options?.requireAuth ?? true,
|
||||
});
|
||||
|
||||
return proxyAuth.proxyRequest(request, path, options);
|
||||
}
|
||||
|
||||
// Helper function to handle proxy responses with error handling
|
||||
export async function handleProxyResponse(
|
||||
request: NextRequest,
|
||||
backendUrl: string,
|
||||
path: string = '',
|
||||
options?: {
|
||||
method?: string;
|
||||
headers?: Record<string, string>;
|
||||
body?: any;
|
||||
requireAuth?: boolean;
|
||||
}
|
||||
): Promise<NextResponse> {
|
||||
try {
|
||||
return await proxyRequest(request, backendUrl, path, options);
|
||||
} catch (error) {
|
||||
console.error('Proxy request failed:', error);
|
||||
return NextResponse.json(
|
||||
{ error: 'Internal server error' },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
99
frontend/src/lib/token-manager.ts
Normal file
99
frontend/src/lib/token-manager.ts
Normal file
@@ -0,0 +1,99 @@
|
||||
import Cookies from 'js-cookie';
|
||||
import { EventEmitter } from 'events';
|
||||
|
||||
interface TokenManagerEvents {
|
||||
tokensUpdated: [];
|
||||
tokensCleared: [];
|
||||
}
|
||||
|
||||
export interface TokenManagerInterface {
|
||||
getTokens(): { access_token: string | null; refresh_token: string | null };
|
||||
setTokens(access_token: string, refresh_token: string): void;
|
||||
clearTokens(): void;
|
||||
isAuthenticated(): boolean;
|
||||
getAccessToken(): string | null;
|
||||
getRefreshToken(): string | null;
|
||||
getTokenExpiry(): { access_token_expiry: number | null; refresh_token_expiry: number | null };
|
||||
on<E extends keyof TokenManagerEvents>(
|
||||
event: E,
|
||||
listener: (...args: TokenManagerEvents[E]) => void
|
||||
): this;
|
||||
off<E extends keyof TokenManagerEvents>(
|
||||
event: E,
|
||||
listener: (...args: TokenManagerEvents[E]) => void
|
||||
): this;
|
||||
}
|
||||
|
||||
class TokenManager extends EventEmitter implements TokenManagerInterface {
|
||||
private static instance: TokenManager;
|
||||
|
||||
private constructor() {
|
||||
super();
|
||||
// Set max listeners to avoid memory leak warnings
|
||||
this.setMaxListeners(100);
|
||||
}
|
||||
|
||||
static getInstance(): TokenManager {
|
||||
if (!TokenManager.instance) {
|
||||
TokenManager.instance = new TokenManager();
|
||||
}
|
||||
return TokenManager.instance;
|
||||
}
|
||||
|
||||
getTokens() {
|
||||
return {
|
||||
access_token: Cookies.get('access_token'),
|
||||
refresh_token: Cookies.get('refresh_token'),
|
||||
};
|
||||
}
|
||||
|
||||
setTokens(access_token: string, refresh_token: string) {
|
||||
// Set cookies with secure attributes
|
||||
Cookies.set('access_token', access_token, {
|
||||
expires: 7, // 7 days
|
||||
secure: process.env.NODE_ENV === 'production',
|
||||
sameSite: 'strict',
|
||||
});
|
||||
|
||||
Cookies.set('refresh_token', refresh_token, {
|
||||
expires: 30, // 30 days
|
||||
secure: process.env.NODE_ENV === 'production',
|
||||
sameSite: 'strict',
|
||||
});
|
||||
|
||||
// Emit event
|
||||
this.emit('tokensUpdated');
|
||||
}
|
||||
|
||||
clearTokens() {
|
||||
Cookies.remove('access_token');
|
||||
Cookies.remove('refresh_token');
|
||||
this.emit('tokensCleared');
|
||||
}
|
||||
|
||||
isAuthenticated(): boolean {
|
||||
return !!this.getAccessToken();
|
||||
}
|
||||
|
||||
getAccessToken(): string | null {
|
||||
return Cookies.get('access_token');
|
||||
}
|
||||
|
||||
getRefreshToken(): string | null {
|
||||
return Cookies.get('refresh_token');
|
||||
}
|
||||
|
||||
getTokenExpiry(): { access_token_expiry: number | null; refresh_token_expiry: number | null } {
|
||||
return {
|
||||
access_token_expiry: parseInt(Cookies.get('access_token_expiry') || '0') || null,
|
||||
refresh_token_expiry: parseInt(Cookies.get('refresh_token_expiry') || '0') || null,
|
||||
};
|
||||
}
|
||||
|
||||
getRefreshTokenExpiry(): number | null {
|
||||
return parseInt(Cookies.get('refresh_token_expiry') || '0') || null;
|
||||
}
|
||||
}
|
||||
|
||||
// Export singleton instance
|
||||
export const tokenManager = TokenManager.getInstance();
|
||||
98
frontend/src/lib/utils.ts
Normal file
98
frontend/src/lib/utils.ts
Normal file
@@ -0,0 +1,98 @@
|
||||
import { type ClassValue, clsx } from "clsx"
|
||||
import { twMerge } from "tailwind-merge"
|
||||
|
||||
export function cn(...inputs: ClassValue[]) {
|
||||
return twMerge(clsx(inputs))
|
||||
}
|
||||
|
||||
export function formatDate(date: Date | string): string {
|
||||
const d = new Date(date);
|
||||
return d.toLocaleDateString('en-US', {
|
||||
year: 'numeric',
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
});
|
||||
}
|
||||
|
||||
export function formatDateTime(date: Date | string): string {
|
||||
const d = new Date(date);
|
||||
return d.toLocaleString('en-US', {
|
||||
year: 'numeric',
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
});
|
||||
}
|
||||
|
||||
export function formatRelativeTime(date: Date | string): string {
|
||||
const d = new Date(date);
|
||||
const now = new Date();
|
||||
const diffMs = now.getTime() - d.getTime();
|
||||
const diffSeconds = Math.floor(diffMs / 1000);
|
||||
const diffMinutes = Math.floor(diffSeconds / 60);
|
||||
const diffHours = Math.floor(diffMinutes / 60);
|
||||
const diffDays = Math.floor(diffHours / 24);
|
||||
|
||||
if (diffSeconds < 60) return 'just now';
|
||||
if (diffMinutes < 60) return `${diffMinutes} minute${diffMinutes > 1 ? 's' : ''} ago`;
|
||||
if (diffHours < 24) return `${diffHours} hour${diffHours > 1 ? 's' : ''} ago`;
|
||||
if (diffDays < 7) return `${diffDays} day${diffDays > 1 ? 's' : ''} ago`;
|
||||
|
||||
return formatDate(d);
|
||||
}
|
||||
|
||||
export function formatBytes(bytes: number, decimals = 2): string {
|
||||
if (bytes === 0) return '0 Bytes';
|
||||
|
||||
const k = 1024;
|
||||
const dm = decimals < 0 ? 0 : decimals;
|
||||
const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB'];
|
||||
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||
|
||||
return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + ' ' + sizes[i];
|
||||
}
|
||||
|
||||
export function formatNumber(num: number): string {
|
||||
return new Intl.NumberFormat('en-US').format(num);
|
||||
}
|
||||
|
||||
export function formatCurrency(amount: number, currency = 'USD'): string {
|
||||
return new Intl.NumberFormat('en-US', {
|
||||
style: 'currency',
|
||||
currency,
|
||||
}).format(amount);
|
||||
}
|
||||
|
||||
export function truncate(str: string, length: number): string {
|
||||
if (str.length <= length) return str;
|
||||
return str.slice(0, length) + '...';
|
||||
}
|
||||
|
||||
export function debounce<T extends (...args: any[]) => any>(
|
||||
func: T,
|
||||
wait: number
|
||||
): (...args: Parameters<T>) => void {
|
||||
let timeout: NodeJS.Timeout;
|
||||
|
||||
return (...args: Parameters<T>) => {
|
||||
clearTimeout(timeout);
|
||||
timeout = setTimeout(() => func(...args), wait);
|
||||
};
|
||||
}
|
||||
|
||||
export function throttle<T extends (...args: any[]) => any>(
|
||||
func: T,
|
||||
limit: number
|
||||
): (...args: Parameters<T>) => void {
|
||||
let inThrottle: boolean;
|
||||
|
||||
return (...args: Parameters<T>) => {
|
||||
if (!inThrottle) {
|
||||
func(...args);
|
||||
inThrottle = true;
|
||||
setTimeout(() => (inThrottle = false), limit);
|
||||
}
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user