Initial commit: igny8 project

This commit is contained in:
igny8
2025-11-09 10:27:02 +00:00
commit 60b8188111
27265 changed files with 4360521 additions and 0 deletions

View File

@@ -0,0 +1,52 @@
/**
* Global Date Formatting Utility
* Formats dates to relative time strings (today, yesterday, etc.)
* Usage: formatRelativeDate('2025-01-15') or formatRelativeDate(new Date())
*/
export function formatRelativeDate(dateString: string | Date): string {
if (!dateString) {
return 'Today';
}
const date = typeof dateString === 'string' ? new Date(dateString) : dateString;
// Check if date is valid
if (isNaN(date.getTime())) {
return 'Today';
}
const now = new Date();
// Set time to midnight for both dates to compare days only
const today = new Date(now.getFullYear(), now.getMonth(), now.getDate());
const dateOnly = new Date(date.getFullYear(), date.getMonth(), date.getDate());
const diffTime = today.getTime() - dateOnly.getTime();
const diffDays = Math.floor(diffTime / (1000 * 60 * 60 * 24));
if (diffDays === 0) {
return 'Today';
} else if (diffDays === 1) {
return 'Yesterday';
} else if (diffDays < 30) {
return `${diffDays} days ago`;
} else if (diffDays < 365) {
const months = Math.floor(diffDays / 30);
const remainingDays = diffDays % 30;
if (remainingDays === 0) {
return `${months} month${months > 1 ? 's' : ''} ago`;
} else {
return `${months} month${months > 1 ? 's' : ''} ${remainingDays} day${remainingDays > 1 ? 's' : ''} ago`;
}
} else {
const years = Math.floor(diffDays / 365);
const remainingMonths = Math.floor((diffDays % 365) / 30);
if (remainingMonths === 0) {
return `${years} year${years > 1 ? 's' : ''} ago`;
} else {
return `${years} year${years > 1 ? 's' : ''} ${remainingMonths} month${remainingMonths > 1 ? 's' : ''} ago`;
}
}
}

View File

@@ -0,0 +1,106 @@
/**
* Global Difficulty Mapping Utility
* Maps numeric difficulty values (0-100) to number scale (1-5)
* 1 = Very Easy (0-10)
* 2 = Easy (11-30)
* 3 = Medium (31-50)
* 4 = Hard (51-70)
* 5 = Very Hard (71-100)
*/
export const DIFFICULTY_RANGES = {
'Very Easy': { min: 0, max: 10, number: 1 },
'Easy': { min: 11, max: 30, number: 2 },
'Medium': { min: 31, max: 50, number: 3 },
'Hard': { min: 51, max: 70, number: 4 },
'Very Hard': { min: 71, max: 100, number: 5 },
} as const;
export type DifficultyLabel = keyof typeof DIFFICULTY_RANGES;
/**
* Convert numeric difficulty (0-100) to number scale (1-5)
*/
export function getDifficultyNumber(difficulty: number | null | undefined): number | string {
if (difficulty === null || difficulty === undefined) {
return '-';
}
if (difficulty <= 10) return 1;
if (difficulty <= 30) return 2;
if (difficulty <= 50) return 3;
if (difficulty <= 70) return 4;
return 5;
}
/**
* Convert numeric difficulty to label (legacy function - still used for filtering)
*/
export function getDifficultyLabel(difficulty: number | null | undefined): DifficultyLabel | string {
if (difficulty === null || difficulty === undefined) {
return '-';
}
if (difficulty <= 10) return 'Very Easy';
if (difficulty <= 30) return 'Easy';
if (difficulty <= 50) return 'Medium';
if (difficulty <= 70) return 'Hard';
return 'Very Hard';
}
/**
* Convert difficulty label to numeric range for filtering
*/
export function getDifficultyRange(label: DifficultyLabel): { min: number; max: number } {
return { min: DIFFICULTY_RANGES[label].min, max: DIFFICULTY_RANGES[label].max };
}
/**
* Get all difficulty labels as array
*/
export function getDifficultyLabels(): DifficultyLabel[] {
return Object.keys(DIFFICULTY_RANGES) as DifficultyLabel[];
}
/**
* Get difficulty number from label (1-5)
*/
export function getDifficultyNumberFromLabel(label: DifficultyLabel): number {
return DIFFICULTY_RANGES[label].number;
}
/**
* Get difficulty label from number (1-5)
*/
export function getDifficultyLabelFromNumber(num: number): DifficultyLabel | null {
for (const [label, range] of Object.entries(DIFFICULTY_RANGES)) {
if (range.number === num) {
return label as DifficultyLabel;
}
}
return null;
}
/**
* Get all difficulty numbers (1-5) with their labels
*/
export function getDifficultyOptions(): Array<{ value: string; label: string; number: number }> {
return [
{ value: '1', label: '1 - Very Easy', number: 1 },
{ value: '2', label: '2 - Easy', number: 2 },
{ value: '3', label: '3 - Medium', number: 3 },
{ value: '4', label: '4 - Hard', number: 4 },
{ value: '5', label: '5 - Very Hard', number: 5 },
];
}
/**
* Convert difficulty number (1-5) to 0-100 range value (using middle of range)
* Used when saving form data where user selected 1-5 dropdown
*/
export function getDifficultyValueFromNumber(num: number): number {
if (num <= 1) return 5; // Very Easy: middle of 0-10
if (num === 2) return 20; // Easy: middle of 11-30
if (num === 3) return 40; // Medium: middle of 31-50
if (num === 4) return 60; // Hard: middle of 51-70
return 85; // Very Hard: middle of 71-100
}

View File

@@ -0,0 +1,70 @@
/**
* HTML Sanitization Utility
* Sanitizes HTML content to prevent XSS attacks
*
* Note: For production, consider using DOMPurify library for more robust sanitization
* For now, this provides basic script tag removal and safe HTML rendering
*/
/**
* Sanitize HTML string by removing dangerous elements and attributes
* @param html - The HTML string to sanitize
* @returns Sanitized HTML string
*/
export function sanitizeHTML(html: string): string {
if (!html) return '';
// Create a temporary div to parse HTML
const tempDiv = document.createElement('div');
tempDiv.innerHTML = html;
// Remove script tags and their contents
const scripts = tempDiv.querySelectorAll('script');
scripts.forEach(script => script.remove());
// Remove event handlers from all elements
const allElements = tempDiv.querySelectorAll('*');
allElements.forEach(el => {
// Remove all event handlers
const attributes = el.attributes;
for (let i = attributes.length - 1; i >= 0; i--) {
const attr = attributes[i];
if (attr.name.startsWith('on')) {
el.removeAttribute(attr.name);
}
// Remove javascript: protocol from href/src
if ((attr.name === 'href' || attr.name === 'src') && attr.value.startsWith('javascript:')) {
el.removeAttribute(attr.name);
}
}
});
return tempDiv.innerHTML;
}
/**
* Check if content appears to be HTML
* @param content - Content to check
* @returns True if content appears to be HTML
*/
export function isHTML(content: string): boolean {
if (!content) return false;
// Check for HTML tags
return /<[a-z][\s\S]*>/i.test(content);
}
/**
* Check if content appears to be JSON
* @param content - Content to check
* @returns True if content appears to be JSON
*/
export function isJSON(content: string): boolean {
if (!content) return false;
try {
JSON.parse(content);
return true;
} catch {
return false;
}
}

View File

@@ -0,0 +1,31 @@
/**
* Global Utilities Index
* Central export point for reusable utilities across the application
*
* This file makes commonly used utilities easily accessible throughout the app
* without needing to know specific file paths.
*/
// Difficulty utilities - used across multiple pages (Keywords, Clusters, Ideas, etc.)
export {
DIFFICULTY_RANGES,
getDifficultyNumber,
getDifficultyLabel,
getDifficultyRange,
getDifficultyLabels,
getDifficultyNumberFromLabel,
getDifficultyLabelFromNumber,
getDifficultyOptions,
getDifficultyValueFromNumber,
type DifficultyLabel,
} from './difficulty';
// Date utilities
export {
formatRelativeDate,
formatDate,
formatDateTime,
} from './date';
// Add other global utilities here as needed

View File

@@ -0,0 +1,251 @@
/**
* Global Table Import/Export Utility
* Handles CSV/JSON export and import for any table-based page
*
* Usage:
* ```typescript
* import { useTableImportExport } from '../../utils/table-import-export';
*
* const { handleExport, handleImport } = useTableImportExport({
* endpoint: '/v1/planner/keywords/export/',
* filename: 'keywords',
* columns: columns, // Column config from TablePageTemplate
* filters: filterValues, // Current filter values
* });
* ```
*/
import { API_BASE_URL } from '../services/api';
export interface ExportConfig {
endpoint: string;
filename: string;
format?: 'csv' | 'json';
columns?: Array<{ key: string; label: string }>;
filters?: Record<string, any>;
}
export interface ImportConfig {
endpoint: string;
acceptedFormats?: string[];
maxFileSize?: number;
queryParams?: Record<string, any>; // Additional query params (e.g., site_id, sector_id)
onSuccess?: (result: any) => void;
onError?: (error: Error) => void;
}
/**
* Build export URL with filters as query parameters
*/
export const buildExportUrl = (config: ExportConfig): string => {
const params = new URLSearchParams();
// Add filters as query params
if (config.filters) {
Object.entries(config.filters).forEach(([key, value]) => {
if (value !== '' && value !== null && value !== undefined) {
params.append(key, String(value));
}
});
}
const queryString = params.toString();
const endpoint = config.endpoint.endsWith('/')
? config.endpoint
: `${config.endpoint}/`;
const fullUrl = `${API_BASE_URL}${endpoint}${queryString ? `?${queryString}` : ''}`;
return fullUrl;
};
/**
* Export table data to CSV or JSON
*/
export const exportTableData = async (
config: ExportConfig,
onProgress?: (progress: string) => void,
onError?: (error: Error) => void
): Promise<void> => {
const format = config.format || 'csv';
const url = buildExportUrl(config);
onProgress?.(`Exporting ${format.toUpperCase()}...`);
try {
const response = await fetch(url, {
method: 'GET',
credentials: 'include',
});
if (!response.ok) {
const errorText = await response.text();
throw new Error(`Export failed: ${response.statusText} - ${errorText}`);
}
const blob = await response.blob();
const downloadUrl = window.URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = downloadUrl;
link.download = `${config.filename}.${format}`;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
window.URL.revokeObjectURL(downloadUrl);
onProgress?.(`Export successful: ${config.filename}.${format}`);
} catch (error) {
const err = error instanceof Error ? error : new Error('Export failed');
onError?.(err);
throw err;
}
};
/**
* Import table data from file
*/
export const importTableData = async (
file: File,
config: ImportConfig,
onProgress?: (progress: string) => void,
onError?: (error: Error) => void
): Promise<any> => {
// Validate file format
const acceptedFormats = config.acceptedFormats || ['.csv'];
const fileExtension = '.' + file.name.split('.').pop()?.toLowerCase();
if (!acceptedFormats.includes(fileExtension)) {
const error = new Error(
`Invalid file format. Accepted formats: ${acceptedFormats.join(', ')}`
);
onError?.(error);
throw error;
}
// Validate file size
const maxSize = config.maxFileSize || 5 * 1024 * 1024; // 5MB default
if (file.size > maxSize) {
const error = new Error(`File size exceeds ${maxSize / 1024 / 1024}MB limit`);
onError?.(error);
throw error;
}
onProgress?.(`Importing ${file.name}...`);
const formData = new FormData();
formData.append('file', file);
try {
const endpoint = config.endpoint.endsWith('/')
? config.endpoint
: `${config.endpoint}/`;
// Build query string from queryParams
const params = new URLSearchParams();
if (config.queryParams) {
Object.entries(config.queryParams).forEach(([key, value]) => {
if (value !== '' && value !== null && value !== undefined) {
params.append(key, String(value));
}
});
}
const queryString = params.toString();
const url = `${API_BASE_URL}${endpoint}${queryString ? `?${queryString}` : ''}`;
const response = await fetch(url, {
method: 'POST',
body: formData,
credentials: 'include',
});
if (!response.ok) {
const errorText = await response.text();
throw new Error(`Import failed: ${response.statusText} - ${errorText}`);
}
const result = await response.json();
onProgress?.(`Import successful: ${result.imported || 0} rows imported`);
config.onSuccess?.(result);
return result;
} catch (error) {
const err = error instanceof Error ? error : new Error('Import failed');
onError?.(err);
throw err;
}
};
/**
* React hook for table import/export functionality
*/
export const useTableImportExport = (exportConfig: ExportConfig) => {
const handleExport = async (
format: 'csv' | 'json' = 'csv',
onProgress?: (progress: string) => void,
onError?: (error: Error) => void
) => {
await exportTableData(
{ ...exportConfig, format },
onProgress,
onError
);
};
const handleImport = async (
file: File,
importConfig: ImportConfig,
onProgress?: (progress: string) => void,
onError?: (error: Error) => void
) => {
return await importTableData(file, importConfig, onProgress, onError);
};
return {
handleExport,
handleImport,
};
};
/**
* Generate CSV from table data (client-side fallback)
* Used when backend doesn't provide export endpoint
*/
export const generateCSVFromTable = (
data: any[],
columns: Array<{ key: string; label: string }>,
filename: string = 'export'
): void => {
// Create header row
const headers = columns.map((col) => col.label).join(',');
// Create data rows
const rows = data.map((row) =>
columns
.map((col) => {
const value = row[col.key];
// Escape commas and quotes in CSV
const stringValue = value?.toString() || '';
if (stringValue.includes(',') || stringValue.includes('"') || stringValue.includes('\n')) {
return `"${stringValue.replace(/"/g, '""')}"`;
}
return stringValue;
})
.join(',')
);
const csvContent = [headers, ...rows].join('\n');
// Create download
const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' });
const downloadUrl = window.URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = downloadUrl;
link.download = `${filename}.csv`;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
window.URL.revokeObjectURL(downloadUrl);
};