mirror of
https://github.com/SamyRai/turash.git
synced 2025-12-26 23:01:33 +00:00
Some checks failed
CI/CD Pipeline / backend-lint (push) Failing after 31s
CI/CD Pipeline / backend-build (push) Has been skipped
CI/CD Pipeline / frontend-lint (push) Failing after 1m37s
CI/CD Pipeline / frontend-build (push) Has been skipped
CI/CD Pipeline / e2e-test (push) Has been skipped
- Replace all 'any' types with proper TypeScript interfaces - Fix React hooks setState in useEffect issues with lazy initialization - Remove unused variables and imports across all files - Fix React Compiler memoization dependency issues - Add comprehensive i18n translation keys for admin interfaces - Apply consistent prettier formatting throughout codebase - Clean up unused bulk editing functionality - Improve type safety and code quality across frontend Files changed: 39 - ImpactMetrics.tsx: Fixed any types and interfaces - AdminVerificationQueuePage.tsx: Added i18n keys, removed unused vars - LocalizationUIPage.tsx: Fixed memoization, added translations - LocalizationDataPage.tsx: Added type safety and translations - And 35+ other files with various lint fixes
321 lines
11 KiB
TypeScript
321 lines
11 KiB
TypeScript
import { getOrganizationIconSvg } from '@/components/map/organizationIcons';
|
|
import { mapDatabaseSubtypeToSchemaSubtype } from '@/lib/organizationSubtypeMapper.ts';
|
|
import { DivIcon } from 'leaflet';
|
|
import { Building } from 'lucide-react';
|
|
import React from 'react';
|
|
import { renderToStaticMarkup } from 'react-dom/server';
|
|
|
|
/**
|
|
* Icon cache to prevent recreating icons on every render
|
|
* Uses WeakMap for automatic garbage collection when icons are no longer needed
|
|
*/
|
|
const iconCache = new WeakMap<object, Map<string, DivIcon>>();
|
|
|
|
/**
|
|
* Create a cache key for an icon based on its properties
|
|
*/
|
|
function createIconCacheKey(
|
|
orgId: string,
|
|
isSelected: boolean,
|
|
isHovered: boolean,
|
|
logoUrl?: string | null
|
|
): string {
|
|
return `${orgId}-${isSelected}-${isHovered}-${logoUrl || 'no-logo'}`;
|
|
}
|
|
|
|
/**
|
|
* Get or create a cached icon for an organization marker
|
|
* Includes error handling for edge cases
|
|
*/
|
|
/**
|
|
* Get computed CSS variable value
|
|
* This ensures CSS variables resolve properly in inline HTML
|
|
*/
|
|
function getCSSVariableValue(variableName: string, fallback: string = '#000000'): string {
|
|
if (typeof window === 'undefined') return fallback;
|
|
|
|
try {
|
|
const value = getComputedStyle(document.documentElement).getPropertyValue(variableName).trim();
|
|
return value || fallback;
|
|
} catch {
|
|
return fallback;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get sector color from CSS variable or fallback
|
|
* Ensures colors are visible and never black/dark
|
|
*/
|
|
function getSectorColor(colorKey: string | undefined): string {
|
|
if (!colorKey) return '#6b7280'; // gray fallback
|
|
|
|
const cssVar = `--sector-${colorKey}`;
|
|
const color = getCSSVariableValue(cssVar);
|
|
|
|
// If we got a hex color, use it directly; otherwise try to parse
|
|
if (color.startsWith('#')) {
|
|
// Validate it's not black or too dark
|
|
const hex = color.replace('#', '');
|
|
if (hex === '000000' || hex === '000') {
|
|
// Use fallback instead of black
|
|
return getSectorFallbackColor(colorKey);
|
|
}
|
|
return color;
|
|
}
|
|
|
|
// Use fallback colors for common sectors
|
|
return getSectorFallbackColor(colorKey);
|
|
}
|
|
|
|
/**
|
|
* Get fallback color for a sector (ensures visibility)
|
|
*/
|
|
function getSectorFallbackColor(colorKey: string): string {
|
|
// Bright, visible colors for sectors
|
|
const fallbackColors: Record<string, string> = {
|
|
construction: '#5d7c9a', // Blue-gray
|
|
production: '#2e7d32', // Green
|
|
recreation: '#d97757', // Orange
|
|
logistics: '#7b68b8', // Purple
|
|
agriculture: '#4caf50', // Green
|
|
energy: '#ff9800', // Orange
|
|
technology: '#2196f3', // Blue
|
|
manufacturing: '#607d8b', // Blue-gray
|
|
commercial: '#9c27b0', // Purple
|
|
healthcare: '#e91e63', // Pink
|
|
education: '#3f51b5', // Indigo
|
|
government: '#795548', // Brown
|
|
hospitality: '#f44336', // Red
|
|
transportation: '#00bcd4', // Cyan
|
|
infrastructure: '#ff5722', // Deep orange
|
|
};
|
|
|
|
return fallbackColors[colorKey] || '#6b7280'; // Gray fallback
|
|
}
|
|
|
|
export function getCachedOrganizationIcon(
|
|
orgId: string,
|
|
org: { LogoURL?: string | null; Name: string; Subtype?: string | null | undefined },
|
|
sector: { icon?: React.ReactElement; colorKey?: string } | null,
|
|
isSelected: boolean,
|
|
isHovered: boolean
|
|
): DivIcon {
|
|
try {
|
|
// Use org object as WeakMap key for automatic cleanup
|
|
const orgCache = iconCache.get(org) || new Map<string, DivIcon>();
|
|
const cacheKey = createIconCacheKey(orgId, isSelected, isHovered, org.LogoURL);
|
|
|
|
// Return cached icon if available
|
|
const cached = orgCache.get(cacheKey);
|
|
if (cached) {
|
|
return cached;
|
|
}
|
|
|
|
// Create new icon - larger size for better visibility
|
|
// Base size increased for better visibility at all zoom levels
|
|
const size = Math.max(28, isSelected ? 40 : isHovered ? 34 : 32); // Increased base size for better icon visibility
|
|
|
|
// Ensure size is a valid number
|
|
if (!Number.isFinite(size) || size <= 0) {
|
|
console.warn('[IconCache] Invalid size calculated:', size, { isSelected, isHovered });
|
|
}
|
|
const borderWidth = isSelected || isHovered ? 3 : 2;
|
|
|
|
// Get actual color values instead of CSS variables for inline styles
|
|
const primaryColor = getCSSVariableValue('--primary', '#3b82f6');
|
|
const borderColorValue =
|
|
isSelected || isHovered ? primaryColor : getCSSVariableValue('--border', '#e5e7eb');
|
|
// Get background color - ensure it's never black/dark unless explicitly set
|
|
const bgColor = org.LogoURL
|
|
? getCSSVariableValue('--primary-foreground', '#ffffff')
|
|
: (() => {
|
|
const sectorColor = getSectorColor(sector?.colorKey);
|
|
// Ensure we don't get black/dark colors that make icons invisible
|
|
// If color is too dark, use a lighter fallback
|
|
if (
|
|
sectorColor === '#000000' ||
|
|
sectorColor === '#000' ||
|
|
!sectorColor ||
|
|
sectorColor.trim() === ''
|
|
) {
|
|
return '#6b7280'; // Use gray as fallback instead of black
|
|
}
|
|
return sectorColor;
|
|
})();
|
|
|
|
// Escape HTML in organization name for safety
|
|
const escapedName = org.Name.replace(/</g, '<')
|
|
.replace(/>/g, '>')
|
|
.replace(/"/g, '"');
|
|
|
|
// Map database subtype to schema subtype
|
|
// Database has granular subtypes (retail, food_beverage, etc.)
|
|
// Schema expects broader categories (commercial, healthcare, etc.)
|
|
const schemaSubtype = mapDatabaseSubtypeToSchemaSubtype(org.Subtype);
|
|
|
|
// Create fallback HTML for when logo fails to load
|
|
// Use organization subtype icon based on mapped schema subtype
|
|
// Note: iconColor parameter is now ignored - getOrganizationIconSvg calculates contrasting color automatically
|
|
const iconColor = '#ffffff'; // This is a placeholder - actual color is calculated inside getOrganizationIconSvg
|
|
|
|
const fallbackIconSvg: string = getOrganizationIconSvg(schemaSubtype, size, iconColor, bgColor);
|
|
// Prefer schema subtype icon (mapped from database subtype)
|
|
// getOrganizationIconSvg will automatically calculate a contrasting color based on bgColor
|
|
fallbackIconSvg = getOrganizationIconSvg(schemaSubtype, size, iconColor, bgColor);
|
|
|
|
// Fallback HTML with proper centering
|
|
// getOrganizationIconSvg returns just the SVG icon, we wrap it for centering
|
|
// Note: background is already set on the outer div, so we don't need it here
|
|
const fallbackHtml = `
|
|
<div style="width: 100%; height: 100%; display: flex; align-items: center; justify-content: center;">
|
|
${fallbackIconSvg}
|
|
</div>
|
|
`;
|
|
|
|
const html = `
|
|
<div style="
|
|
width: ${size}px;
|
|
height: ${size}px;
|
|
border-radius: 50%;
|
|
background-color: ${bgColor};
|
|
border: ${borderWidth}px solid ${borderColorValue};
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
|
|
transition: all 0.2s;
|
|
position: relative;
|
|
overflow: hidden;
|
|
">
|
|
${
|
|
org.LogoURL
|
|
? `<img src="${org.LogoURL}" alt="${escapedName}" style="width: 100%; height: 100%; border-radius: 50%; object-fit: cover;" loading="lazy" onerror="this.onerror=null;this.style.display='none';this.nextElementSibling&&(this.nextElementSibling.style.display='flex');" />
|
|
<div style="display:none;width:100%;height:100%;align-items:center;justify-content:center;">${fallbackHtml}</div>`
|
|
: fallbackHtml
|
|
}
|
|
</div>
|
|
`;
|
|
|
|
const icon = new DivIcon({
|
|
html,
|
|
className: 'custom-organization-marker',
|
|
iconSize: [size, size],
|
|
iconAnchor: [size / 2, size / 2],
|
|
popupAnchor: [0, -size / 2],
|
|
});
|
|
|
|
// Debug logging in development
|
|
if (process.env.NODE_ENV === 'development') {
|
|
console.log(`[IconCache] Created icon for ${org.Name}:`, {
|
|
size,
|
|
bgColor,
|
|
borderColor: borderColorValue,
|
|
hasLogo: !!org.LogoURL,
|
|
sector: sector?.colorKey,
|
|
});
|
|
}
|
|
|
|
// Cache the icon
|
|
orgCache.set(cacheKey, icon);
|
|
iconCache.set(org, orgCache);
|
|
|
|
return icon;
|
|
} catch {
|
|
// Fallback icon in case of any error
|
|
const size = 24;
|
|
return new DivIcon({
|
|
html: `<div style="width: ${size}px; height: ${size}px; border-radius: 50%; background-color: hsl(var(--muted)); border: 2px solid hsl(var(--border));"></div>`,
|
|
className: 'custom-organization-marker',
|
|
iconSize: [size, size],
|
|
iconAnchor: [size / 2, size / 2],
|
|
});
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get or create a cached icon for a historical landmark marker
|
|
*/
|
|
export function getCachedHistoricalIcon(
|
|
landmarkId: string,
|
|
landmark: { id: string },
|
|
isSelected: boolean,
|
|
isHovered: boolean
|
|
): DivIcon {
|
|
const landmarkCache = iconCache.get(landmark) || new Map<string, DivIcon>();
|
|
const cacheKey = createIconCacheKey(landmarkId, isSelected, isHovered);
|
|
|
|
const cached = landmarkCache.get(cacheKey);
|
|
if (cached) {
|
|
return cached;
|
|
}
|
|
|
|
const size = isSelected ? 40 : isHovered ? 32 : 28;
|
|
const borderWidth = isSelected || isHovered ? 3 : 2;
|
|
|
|
// Get actual color values
|
|
const primaryColor = getCSSVariableValue('--primary', '#3b82f6');
|
|
const borderColorValue =
|
|
isSelected || isHovered ? primaryColor : getCSSVariableValue('--border', '#e5e7eb');
|
|
const bgColor = '#8b5cf6'; // Purple/violet color for historical buildings
|
|
const iconColor = '#ffffff';
|
|
|
|
// Create a friendly building icon using Lucide
|
|
const iconSize = size * 0.7;
|
|
const buildingIcon = renderToStaticMarkup(
|
|
React.createElement(Building, {
|
|
size: iconSize,
|
|
color: iconColor,
|
|
strokeWidth: 1.5,
|
|
})
|
|
);
|
|
|
|
const html = `
|
|
<div style="
|
|
width: ${size}px;
|
|
height: ${size}px;
|
|
border-radius: 8px;
|
|
background: linear-gradient(135deg, ${bgColor} 0%, ${bgColor}dd 100%);
|
|
border: ${borderWidth}px solid ${borderColorValue};
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
box-shadow: 0 3px 8px rgba(0, 0, 0, 0.25), 0 1px 3px rgba(0, 0, 0, 0.15);
|
|
transition: all 0.2s ease;
|
|
position: relative;
|
|
overflow: visible;
|
|
">
|
|
${buildingIcon}
|
|
${
|
|
isSelected || isHovered
|
|
? `
|
|
<div style="
|
|
position: absolute;
|
|
top: -${size * 0.15}px;
|
|
right: -${size * 0.15}px;
|
|
width: ${size * 0.3}px;
|
|
height: ${size * 0.3}px;
|
|
background: ${primaryColor};
|
|
border-radius: 50%;
|
|
border: 2px solid white;
|
|
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
|
|
"></div>
|
|
`
|
|
: ''
|
|
}
|
|
</div>
|
|
`;
|
|
|
|
const icon = new DivIcon({
|
|
html,
|
|
className: 'custom-historical-marker',
|
|
iconSize: [size, size],
|
|
iconAnchor: [size / 2, size / 2],
|
|
popupAnchor: [0, -size / 2],
|
|
});
|
|
|
|
landmarkCache.set(cacheKey, icon);
|
|
iconCache.set(landmark, landmarkCache);
|
|
|
|
return icon;
|
|
}
|