diff --git a/bugulma/frontend/components/add-organization/steps/Step1.tsx b/bugulma/frontend/components/add-organization/steps/Step1.tsx index 787f51e..7bfafb6 100644 --- a/bugulma/frontend/components/add-organization/steps/Step1.tsx +++ b/bugulma/frontend/components/add-organization/steps/Step1.tsx @@ -74,7 +74,7 @@ const Step1 = ({ )} />

- Upload additional images to showcase your organization (optional) + {t('organization.galleryImagesHint')}

diff --git a/bugulma/frontend/components/admin/ActivityFeed.tsx b/bugulma/frontend/components/admin/ActivityFeed.tsx index 6fa9f90..7d193de 100644 --- a/bugulma/frontend/components/admin/ActivityFeed.tsx +++ b/bugulma/frontend/components/admin/ActivityFeed.tsx @@ -128,10 +128,10 @@ export const ActivityFeed = ({ } return ( - - - Recent Activity - + + + {t?.('activityFeed.recentActivity') || 'Recent Activity'} +
{activities.map((activity) => ( diff --git a/bugulma/frontend/components/admin/DataTable.tsx b/bugulma/frontend/components/admin/DataTable.tsx index dffa2eb..10b9929 100644 --- a/bugulma/frontend/components/admin/DataTable.tsx +++ b/bugulma/frontend/components/admin/DataTable.tsx @@ -3,6 +3,7 @@ import { clsx } from 'clsx'; import { ResponsiveTable, Pagination, SearchBar, Checkbox, Button } from '@/components/ui'; import { MoreVertical } from 'lucide-react'; import { DropdownMenu } from '@/components/ui'; +import { useTranslation } from '@/hooks/useI18n.tsx'; export interface DataTableColumn { key: string; @@ -90,6 +91,7 @@ export function DataTable({ renderMobileCard, className, }: DataTableProps) { + const { t } = useTranslation(); const selectedCount = selection?.selectedRows.size || 0; const hasSelection = selectedCount > 0; @@ -213,7 +215,7 @@ export function DataTable({ {/* Bulk Actions */} {hasSelection && bulkActions && bulkActions.length > 0 && (
- {selectedCount} selected + {t('dataTable.selected', { count: selectedCount })} {bulkActions.map((action, index) => (
)} @@ -251,7 +253,7 @@ export function DataTable({ }} onChange={(e) => handleSelectAll(e.target.checked)} /> - Select all ({data.length} items) + {t('dataTable.selectAll', { count: data.length })}
)} @@ -290,7 +292,7 @@ export function DataTable({ /> {pagination.onPageSizeChange && (
- Items per page: + {t('dataTable.itemsPerPage')} - Reset Filters + {t('heritage.resetFilters')} )}
@@ -184,7 +185,7 @@ const TimelineSection = ({ ) : (

- No events match your filters. Try adjusting your selection. + {t('heritage.noEventsMatch')}

)} diff --git a/bugulma/frontend/components/landing/ResourceExchangeVisualization.tsx b/bugulma/frontend/components/landing/ResourceExchangeVisualization.tsx index a19c6a5..71a8843 100644 --- a/bugulma/frontend/components/landing/ResourceExchangeVisualization.tsx +++ b/bugulma/frontend/components/landing/ResourceExchangeVisualization.tsx @@ -295,8 +295,8 @@ const ResourceExchangeVisualization: React.FC {/* Title */}
-

Resource Exchange Network

-

Businesses connect to exchange resources

+

{t('resourceExchange.networkTitle')}

+

{t('resourceExchange.networkDescription')}

{/* SVG Canvas for network visualization */} @@ -648,7 +648,7 @@ const ResourceExchangeVisualization: React.FC {sectorConnections.length} - {sectorConnections.length} resource exchange connections + {t('resourceExchange.connectionsCount', { count: sectorConnections.length })} )} @@ -754,7 +754,7 @@ const ResourceExchangeVisualization: React.FC - Resource Exchanges: + {t('resourceExchange.resourceExchanges')} {RESOURCE_TYPES.map((resource) => { const Icon = resource.icon; return ( diff --git a/bugulma/frontend/components/map/HistoricalSidebarPreview.tsx b/bugulma/frontend/components/map/HistoricalSidebarPreview.tsx index ed13ce2..e0a1877 100644 --- a/bugulma/frontend/components/map/HistoricalSidebarPreview.tsx +++ b/bugulma/frontend/components/map/HistoricalSidebarPreview.tsx @@ -64,13 +64,13 @@ const HistoricalSidebarPreview = () => {
-

Местоположение и статус

+

{t('mapSidebar.locationAndStatus')}

{relatedOrg && ( <> -
Связанная организация
+
{t('mapSidebar.relatedOrganization')}
{relatedOrg.Name}
diff --git a/bugulma/frontend/components/map/ProductServiceMarkers.tsx b/bugulma/frontend/components/map/ProductServiceMarkers.tsx index e608f50..ca4d3ea 100644 --- a/bugulma/frontend/components/map/ProductServiceMarkers.tsx +++ b/bugulma/frontend/components/map/ProductServiceMarkers.tsx @@ -5,6 +5,7 @@ import { Package, Wrench } from 'lucide-react'; import React, { useCallback, useEffect, useMemo, useState } from 'react'; import { renderToStaticMarkup } from 'react-dom/server'; import { Marker, Popup } from 'react-leaflet'; +import { useTranslation } from '@/hooks/useI18n.tsx'; interface ProductServiceMarkersProps { showProducts: boolean; @@ -19,6 +20,7 @@ const ProductMarker = React.memo<{ isSelected: boolean; onSelect: (match: DiscoveryMatch) => void; }>(({ match, isSelected, onSelect }) => { + const { t } = useTranslation(); const position: LatLngTuple = useMemo(() => { if (!match.product?.location) return [0, 0]; return [match.product.location.latitude, match.product.location.longitude]; @@ -89,7 +91,7 @@ const ProductMarker = React.memo<{
€{match.product.unit_price.toFixed(2)} {match.product.moq > 0 && ( - MOQ: {match.product.moq} + {t('productService.moq', { value: match.product.moq })} )}
{match.organization && ( diff --git a/bugulma/frontend/hooks/pages/useOrganizationPage.ts b/bugulma/frontend/hooks/pages/useOrganizationPage.ts index 1de3913..5377ba3 100644 --- a/bugulma/frontend/hooks/pages/useOrganizationPage.ts +++ b/bugulma/frontend/hooks/pages/useOrganizationPage.ts @@ -20,12 +20,12 @@ export const useOrganizationPage = (organization: Organization | undefined) => { const handleAnalyzeSymbiosis = useCallback(() => { if (!isAuthenticated) return; ai.handleAnalyzeSymbiosis(); - }, [isAuthenticated, ai.handleAnalyzeSymbiosis]); + }, [isAuthenticated, ai]); const handleFetchWebIntelligence = useCallback(() => { if (!isAuthenticated) return; ai.handleFetchWebIntelligence(); - }, [isAuthenticated, ai.handleFetchWebIntelligence]); + }, [isAuthenticated, ai]); return { symbiosisState: ai.symbiosisState, diff --git a/bugulma/frontend/hooks/useKeyboard.ts b/bugulma/frontend/hooks/useKeyboard.ts index 77920ed..9376c01 100644 --- a/bugulma/frontend/hooks/useKeyboard.ts +++ b/bugulma/frontend/hooks/useKeyboard.ts @@ -1,4 +1,4 @@ -import { useCallback, useEffect } from 'react'; +import { useEffect, useRef } from 'react'; interface UseKeyboardOptions { enabled?: boolean; @@ -13,15 +13,23 @@ export function useKeyboard( options: UseKeyboardOptions = {} ) { const { enabled = true, target = document } = options; + const handlerRef = useRef(handler); - const memoizedHandler = useCallback(handler, [handler]); + // Update handler ref when handler changes + useEffect(() => { + handlerRef.current = handler; + }, [handler]); useEffect(() => { if (!enabled) return; - target.addEventListener('keydown', memoizedHandler); - return () => target.removeEventListener('keydown', memoizedHandler); - }, [enabled, target, memoizedHandler]); + const wrappedHandler = (event: KeyboardEvent) => { + handlerRef.current(event); + }; + + target.addEventListener('keydown', wrappedHandler); + return () => target.removeEventListener('keydown', wrappedHandler); + }, [enabled, target]); } /** diff --git a/bugulma/frontend/hooks/useOrganizationFilter.ts b/bugulma/frontend/hooks/useOrganizationFilter.ts index 5faf83b..20b7229 100644 --- a/bugulma/frontend/hooks/useOrganizationFilter.ts +++ b/bugulma/frontend/hooks/useOrganizationFilter.ts @@ -166,7 +166,7 @@ export const useOrganizationFilter = ( return 0; } }); - }, [organizations, debouncedSearchTerm, selectedSectors, sortOption, t]); + }, [organizations, debouncedSearchTerm, selectedSectors, sortOption]); return filteredAndSortedOrgs; }; diff --git a/bugulma/frontend/lib/api-client.ts b/bugulma/frontend/lib/api-client.ts index c7dbbde..2f41176 100644 --- a/bugulma/frontend/lib/api-client.ts +++ b/bugulma/frontend/lib/api-client.ts @@ -89,10 +89,7 @@ function parseApiResponse(response: Response, schema: z.ZodSchema): Promis /** * Schema-validated API GET request */ -export async function apiGetValidated( - endpoint: string, - schema: z.ZodSchema -): Promise { +export async function apiGetValidated(endpoint: string, schema: z.ZodSchema): Promise { const data = await apiGet(endpoint); const result = schema.safeParse(data); if (result.success) { diff --git a/bugulma/frontend/lib/api-config.ts b/bugulma/frontend/lib/api-config.ts index 731829b..156b24b 100644 --- a/bugulma/frontend/lib/api-config.ts +++ b/bugulma/frontend/lib/api-config.ts @@ -3,7 +3,6 @@ * Centralizes API endpoint configuration for consistent routing across all services */ - // API Version Configuration export const API_CONFIG = { VERSION: 'v1', diff --git a/bugulma/frontend/lib/error-handling.ts b/bugulma/frontend/lib/error-handling.ts index 08978a8..d9a9426 100644 --- a/bugulma/frontend/lib/error-handling.ts +++ b/bugulma/frontend/lib/error-handling.ts @@ -350,17 +350,20 @@ export class ErrorHandler { */ private sendToSentry(error: AppError): void { // Placeholder for Sentry integration - if (typeof window !== 'undefined' && (window as any).Sentry) { - (window as any).Sentry.captureException(error.originalError || new Error(error.message), { - tags: { - category: error.category, - severity: error.severity, - }, - extra: { - errorId: error.id, - context: error.context, - }, - }); + if (typeof window !== 'undefined') { + const sentry = (window as Window & { Sentry?: { captureException: (error: Error, context?: Record) => void } }).Sentry; + if (sentry) { + sentry.captureException(error.originalError || new Error(error.message), { + tags: { + category: error.category, + severity: error.severity, + }, + extra: { + errorId: error.id, + context: error.context, + }, + }); + } } } @@ -369,11 +372,14 @@ export class ErrorHandler { */ private sendToAnalytics(error: AppError): void { // Placeholder for analytics integration - if (typeof window !== 'undefined' && (window as any).gtag) { - (window as any).gtag('event', 'exception', { - description: error.message, - fatal: error.severity === ErrorSeverity.CRITICAL, - }); + if (typeof window !== 'undefined') { + const gtag = (window as Window & { gtag?: (event: string, name: string, params: Record) => void }).gtag; + if (gtag) { + gtag('event', 'exception', { + description: error.message, + fatal: error.severity === ErrorSeverity.CRITICAL, + }); + } } } diff --git a/bugulma/frontend/lib/http-client.ts b/bugulma/frontend/lib/http-client.ts index 7f34565..b86f17e 100644 --- a/bugulma/frontend/lib/http-client.ts +++ b/bugulma/frontend/lib/http-client.ts @@ -401,7 +401,7 @@ export const httpClient = { }); if (!response.ok) { - let errorData: any; + let errorData: { error?: string } | string; try { errorData = await response.json(); } catch { diff --git a/bugulma/frontend/lib/security.ts b/bugulma/frontend/lib/security.ts index d25f586..d3697b7 100644 --- a/bugulma/frontend/lib/security.ts +++ b/bugulma/frontend/lib/security.ts @@ -141,7 +141,7 @@ export class InputSanitizer { export class SecurityMonitor { private static violations: SecurityViolation[] = []; - static logViolation(type: string, details: Record): void { + static logViolation(type: string, details: Record): void { const violation: SecurityViolation = { type, details, @@ -182,7 +182,7 @@ export class SecurityMonitor { interface SecurityViolation { type: string; - details: Record; + details: Record; timestamp: string; userAgent: string; url: string; diff --git a/bugulma/frontend/lib/type-safety.ts b/bugulma/frontend/lib/type-safety.ts index e326316..a5ee365 100644 --- a/bugulma/frontend/lib/type-safety.ts +++ b/bugulma/frontend/lib/type-safety.ts @@ -3,7 +3,7 @@ * Provides enhanced type safety with runtime validation and better generic constraints */ -import { z, ZodSchema, ZodTypeAny } from 'zod'; +import { z, ZodSchema } from 'zod'; // Type-safe API response wrapper export interface ApiResponse { diff --git a/bugulma/frontend/locales/en.ts b/bugulma/frontend/locales/en.ts index bbf5f5f..90117c8 100644 --- a/bugulma/frontend/locales/en.ts +++ b/bugulma/frontend/locales/en.ts @@ -53,6 +53,7 @@ export const en = { organization: { logo: 'Logo', galleryImages: 'Gallery Images', + galleryImagesHint: 'Upload additional images to showcase your organization (optional)', }, hero: { kicker: 'Open Beta', @@ -202,6 +203,28 @@ export const en = { activityFeed: { recentActivity: 'Recent Activity', }, + dataTable: { + selected: '{{count}} selected', + clear: 'Clear', + selectAll: 'Select all ({{count}} items)', + itemsPerPage: 'Items per page:', + }, + filterBar: { + filters: 'Filters', + clearAll: 'Clear All', + cancel: 'Cancel', + applyFilters: 'Apply Filters', + }, + adminPanel: { + title: 'Admin Panel', + maintenanceModeActive: 'Maintenance mode is active', + }, + permissionGate: { + noPermission: "You don't have permission to view this content.", + }, + form: { + removeItem: 'Remove item', + }, time: { justNow: 'just now', minutesAgo_one: '{{count}} minute ago', @@ -343,6 +366,9 @@ export const en = { fetchWebIntelButton: 'Get Summary', webIntelSources: 'Sources:', viewOrganization: 'Go to organization', + locationAndStatus: 'Местоположение и статус', + relatedOrganization: 'Связанная организация', + historicalContext: 'Исторический контекст', }, }, chatbot: { @@ -802,6 +828,15 @@ export const en = { floorArea: 'Floor Area', location: 'Location', coordinates: 'Coordinates', + view: 'View', + filters: 'Filters', + eventsCount: '({{count}} events)', + toggleFilters: 'Toggle filters', + category: 'Category', + all: 'All', + minimumImportance: 'Minimum Importance: {{value}}', + resetFilters: 'Reset Filters', + noEventsMatch: 'No events match your filters. Try adjusting your selection.', sources: 'Sources & References', }, similarOrganizations: { @@ -1094,6 +1129,7 @@ export const en = { noMatches: 'No matches found', adjustFilters: 'Try adjusting your filters', hideFilters: 'Hide', + distanceValue: '{{distance}} km', showFilters: 'Filters', matchId: 'Match ID', compatibility: 'Compatibility', @@ -1153,6 +1189,7 @@ export const en = { resourceFlows: 'Resource Flows', matches: 'Matches', co2Saved: 'CO₂ Saved', + co2Unit: 't', perYear: 'per year', economicValue: 'Economic Value', created: 'created annually', @@ -1493,4 +1530,13 @@ export const en = { lastYear: 'Last Year', allTime: 'All Time', }, + resourceExchange: { + networkTitle: 'Resource Exchange Network', + networkDescription: 'Businesses connect to exchange resources', + connectionsCount: '{{count}} resource exchange connections', + resourceExchanges: 'Resource Exchanges:', + }, + productService: { + moq: 'MOQ: {{value}}', + }, }; diff --git a/bugulma/frontend/pages/CommunityEventsPage.tsx b/bugulma/frontend/pages/CommunityEventsPage.tsx index f0b9cba..1010b5a 100644 --- a/bugulma/frontend/pages/CommunityEventsPage.tsx +++ b/bugulma/frontend/pages/CommunityEventsPage.tsx @@ -1,11 +1,13 @@ +import { useTranslation } from '@/hooks/useI18n'; + const CommunityEventsPage = () => { + const { t } = useTranslation(); return (
-

Community Events

+

{t('community.events.title')}

- Community events calendar coming soon! Find sustainability workshops, networking events, - and environmental awareness campaigns. + {t('community.events.description')}

diff --git a/bugulma/frontend/pages/CommunityImpactPage.tsx b/bugulma/frontend/pages/CommunityImpactPage.tsx index 701470e..f4feaa3 100644 --- a/bugulma/frontend/pages/CommunityImpactPage.tsx +++ b/bugulma/frontend/pages/CommunityImpactPage.tsx @@ -1,11 +1,13 @@ +import { useTranslation } from '@/hooks/useI18n'; + const CommunityImpactPage = () => { + const { t } = useTranslation(); return (
-

Community Impact Dashboard

+

{t('community.impact.title')}

- Community impact dashboard coming soon! Track environmental and economic benefits of - industrial symbiosis. + {t('community.impact.description')}

diff --git a/bugulma/frontend/pages/CommunityNewsPage.tsx b/bugulma/frontend/pages/CommunityNewsPage.tsx index a830aae..866742d 100644 --- a/bugulma/frontend/pages/CommunityNewsPage.tsx +++ b/bugulma/frontend/pages/CommunityNewsPage.tsx @@ -1,10 +1,13 @@ +import { useTranslation } from '@/hooks/useI18n'; + const CommunityNewsPage = () => { + const { t } = useTranslation(); return (
-

Community News

+

{t('community.news.title')}

- Community news feature coming soon! Stay tuned for local sustainability news and updates. + {t('community.news.description')}

diff --git a/bugulma/frontend/pages/CommunityStoriesPage.tsx b/bugulma/frontend/pages/CommunityStoriesPage.tsx index 7899754..5c110ee 100644 --- a/bugulma/frontend/pages/CommunityStoriesPage.tsx +++ b/bugulma/frontend/pages/CommunityStoriesPage.tsx @@ -1,11 +1,13 @@ +import { useTranslation } from '@/hooks/useI18n'; + const CommunityStoriesPage = () => { + const { t } = useTranslation(); return (
-

Community Stories

+

{t('community.stories.title')}

- Success stories and case studies coming soon! Read about successful resource connections - and sustainability achievements. + {t('community.stories.description')}

diff --git a/bugulma/frontend/pages/DashboardPage.tsx b/bugulma/frontend/pages/DashboardPage.tsx index d093b77..33c088d 100644 --- a/bugulma/frontend/pages/DashboardPage.tsx +++ b/bugulma/frontend/pages/DashboardPage.tsx @@ -118,6 +118,7 @@ const DashboardPage = () => { proposalsData, userOrganizations, pendingProposals, + proposals.length, ]); // Filter activities based on selected filter @@ -144,7 +145,6 @@ const DashboardPage = () => { // Number formatting handled by shared utilities inside components const isLoading = isLoadingDashboard || isLoadingPlatform || isLoadingMatching || isLoadingImpact; - const hasError = dashboardError || platformError || matchingError || impactError; return ( diff --git a/bugulma/frontend/pages/DiscoveryPage.tsx b/bugulma/frontend/pages/DiscoveryPage.tsx index 0f61737..6e05c31 100644 --- a/bugulma/frontend/pages/DiscoveryPage.tsx +++ b/bugulma/frontend/pages/DiscoveryPage.tsx @@ -16,7 +16,7 @@ import { Heading, Text } from '@/components/ui/Typography'; import { useTranslation } from '@/hooks/useI18n'; import { useNavigation } from '@/hooks/useNavigation'; import { universalSearch, type DiscoveryMatch, type SearchQuery } from '@/services/discovery-api'; -import { MapPin, Package, Users, Wrench } from 'lucide-react'; +import { Package, Users, Wrench } from 'lucide-react'; import { useEffect, useState } from 'react'; import { useSearchParams } from 'react-router-dom'; @@ -92,6 +92,7 @@ export default function DiscoveryPage() { // This is not a "search" so hasActiveSearch will be false handleSearch({ limit: 20 }); } + // eslint-disable-next-line react-hooks/exhaustive-deps }, []); const renderMatchCard = (match: DiscoveryMatch) => { diff --git a/bugulma/frontend/pages/HeritageBuildingPage.tsx b/bugulma/frontend/pages/HeritageBuildingPage.tsx index 1ea0a00..95917e0 100644 --- a/bugulma/frontend/pages/HeritageBuildingPage.tsx +++ b/bugulma/frontend/pages/HeritageBuildingPage.tsx @@ -12,11 +12,10 @@ import { Ruler, User, } from 'lucide-react'; -import { useEffect, useState } from 'react'; +import { useMemo } from 'react'; import { useNavigate, useParams } from 'react-router-dom'; import { MainLayout } from '@/components/layout/MainLayout.tsx'; import Badge from '@/components/ui/Badge.tsx'; -import Spinner from '@/components/ui/Spinner'; import { Heading, Text } from '@/components/ui/Typography.tsx'; import { LoadingState } from '@/components/ui/LoadingState.tsx'; import { useHeritageSites } from '@/hooks/api/useHeritageSitesAPI'; @@ -28,13 +27,12 @@ const HeritageBuildingPage = () => { const navigate = useNavigate(); const { data: heritageSites, isLoading } = useHeritageSites(); const { t } = useTranslation(); - const [building, setBuilding] = useState(null); - - useEffect(() => { + + const building = useMemo(() => { if (heritageSites && id) { - const foundBuilding = heritageSites.find((site) => String(site.ID) === id); - setBuilding(foundBuilding || null); + return heritageSites.find((site) => String(site.ID) === id) || null; } + return null; }, [heritageSites, id]); if (isLoading) { @@ -338,7 +336,7 @@ const HeritageBuildingPage = () => { {t('heritage.floorArea') || 'Floor Area'} - {building.FloorAreaM2} m² + {t('heritage.floorAreaValue', { value: building.FloorAreaM2 })} diff --git a/bugulma/frontend/pages/ImpactMetrics.tsx b/bugulma/frontend/pages/ImpactMetrics.tsx index 8f519ef..5b3e31f 100644 --- a/bugulma/frontend/pages/ImpactMetrics.tsx +++ b/bugulma/frontend/pages/ImpactMetrics.tsx @@ -11,6 +11,57 @@ import { Award, Briefcase, DollarSign, Globe, Target, TrendingUp, Zap } from 'lu import { useMemo } from 'react'; import { formatCurrency, formatNumber } from '../lib/fin'; +// Types for impact metrics data +interface TopImpactingMatch { + id?: string; + description?: string; + resource_type?: string; + co2_impact?: number; + economic_impact?: number; +} + +interface YearlyProjection { + co2_projected?: number; + economic_projected?: number; +} + +// Simple visualization component for impact breakdown +const ImpactBreakdownChart = ({ + data, + title, + color = 'hsl(var(--primary))', +}: { + data: Record; + title: string; + color?: string; +}) => { + const entries = Object.entries(data); + const maxValue = Math.max(...entries.map(([, value]) => value)); + + return ( +
+

{title}

+
+ {entries.map(([key, value]) => ( +
+ {key.replace('_', ' ')} +
+
+
+ {formatNumber(value)} +
+ ))} +
+
+ ); +}; + const ImpactMetrics = () => { const { t } = useTranslation(); const { handleBackNavigation, handleFooterNavigate } = useNavigation(); @@ -54,45 +105,6 @@ const ImpactMetrics = () => { }; }, [impactMetrics, platformStats]); - // using central fin utilities - - // Simple visualization component for impact breakdown - const ImpactBreakdownChart = ({ - data, - title, - color = 'hsl(var(--primary))', - }: { - data: Record; - title: string; - color?: string; - }) => { - const entries = Object.entries(data); - const maxValue = Math.max(...entries.map(([_, value]) => value)); - - return ( -
-

{title}

-
- {entries.map(([key, value]) => ( -
- {key.replace('_', ' ')} -
-
-
- {formatNumber(value)} -
- ))} -
-
- ); - }; - // Impact category icons const getCategoryIcon = (category: string) => { switch (category.toLowerCase()) { @@ -315,7 +327,7 @@ const ImpactMetrics = () => {
- {impact.topImpactingMatches.slice(0, 5).map((match: any, index: number) => ( + {impact.topImpactingMatches.slice(0, 5).map((match: TopImpactingMatch, index: number) => (
{ {index + 1}
-

{match.description || `Match ${match.id}`}

+

{match.description || `Match ${match.id || index}`}

{match.resource_type}

- {formatNumber(match.co2_impact || 0)} t CO₂ + {t('impactMetrics.co2Tonnes', { value: formatNumber(match.co2_impact || 0) })}

{formatCurrency(match.economic_impact || 0)} @@ -382,7 +394,7 @@ const ImpactMetrics = () => { {Object.keys(impact.yearlyProjections).length > 0 ? (

{Object.entries(impact.yearlyProjections).map( - ([year, projection]: [string, any]) => ( + ([year, projection]: [string, YearlyProjection]) => (
{ {year}
- {formatNumber(projection.co2_projected || 0)} t CO₂ + {t('impactMetrics.co2Tonnes', { value: formatNumber(projection.co2_projected || 0) })}
{formatCurrency(projection.economic_projected || 0)} @@ -455,8 +467,7 @@ const ImpactMetrics = () => {

- {t('impactMetrics.activeConnections')} - .replace('{{ count }}', impact.activeMatchesCount.toString()) + {t('impactMetrics.activeConnections', { count: impact.activeMatchesCount })}

diff --git a/bugulma/frontend/pages/LoginPage.tsx b/bugulma/frontend/pages/LoginPage.tsx index a0668be..d35fc16 100644 --- a/bugulma/frontend/pages/LoginPage.tsx +++ b/bugulma/frontend/pages/LoginPage.tsx @@ -105,7 +105,7 @@ const LoginPage = () => { {isDevelopment && (

- Quick Login (Development) + {t('login.quickLogin')}

{TEST_USERS.map((testUser) => ( @@ -129,7 +129,7 @@ const LoginPage = () => { ))}
-

Test Credentials:

+

{t('login.testCredentials')}

{TEST_USERS.map((user) => (
diff --git a/bugulma/frontend/pages/MatchDetailPage.tsx b/bugulma/frontend/pages/MatchDetailPage.tsx index 1d008d5..1089730 100644 --- a/bugulma/frontend/pages/MatchDetailPage.tsx +++ b/bugulma/frontend/pages/MatchDetailPage.tsx @@ -24,13 +24,12 @@ import { MapPin, TrendingUp, } from 'lucide-react'; -import { useMemo, useState } from 'react'; -import { useNavigate, useParams } from 'react-router-dom'; +import { useCallback, useMemo, useState } from 'react'; +import { useParams } from 'react-router-dom'; import { formatCurrency } from '../lib/fin'; const MatchDetailPage = () => { const { id: matchId } = useParams<{ id: string }>(); - const navigate = useNavigate(); const { t } = useTranslation(); const { handleBackNavigation, handleFooterNavigate } = useNavigation(); const { user } = useAuth(); @@ -42,6 +41,19 @@ const MatchDetailPage = () => { const [newStatus, setNewStatus] = useState(''); const [statusNotes, setStatusNotes] = useState(''); + const getHistoryTitle = useCallback((action: string, value?: string) => { + switch (action) { + case 'status_change': + return t('matchDetail.statusChanged'); + case 'comment': + return t('matchDetail.commentAdded'); + case 'update': + return t('matchDetail.matchUpdated'); + default: + return value || action; + } + }, [t]); + // Transform match history to timeline format const timelineEntries: TimelineEntry[] = useMemo(() => { if (!match?.History) return []; @@ -56,20 +68,7 @@ const MatchDetailPage = () => { oldValue: entry.old_value, newValue: entry.new_value, })); - }, [match?.History]); - - const getHistoryTitle = (action: string, value?: string) => { - switch (action) { - case 'status_change': - return t('matchDetail.statusChanged'); - case 'comment': - return t('matchDetail.commentAdded'); - case 'update': - return t('matchDetail.matchUpdated'); - default: - return value || action; - } - }; + }, [match?.History, getHistoryTitle]); const handleStatusUpdate = async () => { if (!match || !newStatus || !user) return; @@ -229,7 +228,7 @@ const MatchDetailPage = () => { {t('matchDetail.paybackPeriod')} - {match.EconomicImpact.payback_years.toFixed(1)} years + {t('matchDetail.paybackYears', { years: match.EconomicImpact.payback_years.toFixed(1) })}
)} diff --git a/bugulma/frontend/pages/MatchNegotiationPage.tsx b/bugulma/frontend/pages/MatchNegotiationPage.tsx index 5f7df01..a8081e5 100644 --- a/bugulma/frontend/pages/MatchNegotiationPage.tsx +++ b/bugulma/frontend/pages/MatchNegotiationPage.tsx @@ -26,7 +26,7 @@ import { MessageSquare, TrendingUp, } from 'lucide-react'; -import React, { useMemo, useState } from 'react'; +import React, { useCallback, useMemo, useState } from 'react'; import { useNavigate, useParams } from 'react-router-dom'; import { formatCurrency } from '../lib/fin'; @@ -46,6 +46,19 @@ const MatchNegotiationPage = () => { const [showMessageModal, setShowMessageModal] = useState(false); const [messageText, setMessageText] = useState(''); + const getHistoryTitle = useCallback((action: string, value?: string) => { + switch (action) { + case 'status_changed': + return t('matchNegotiation.statusChanged'); + case 'comment': + return t('matchNegotiation.commentAdded'); + case 'update': + return t('matchNegotiation.matchUpdated'); + default: + return value || action; + } + }, [t]); + // Transform match history to timeline format const timelineEntries: TimelineEntry[] = useMemo(() => { if (!match?.History) return []; @@ -60,20 +73,7 @@ const MatchNegotiationPage = () => { oldValue: entry.old_value, newValue: entry.new_value, })); - }, [match?.History]); - - const getHistoryTitle = (action: string, value?: string) => { - switch (action) { - case 'status_changed': - return t('matchNegotiation.statusChanged'); - case 'comment': - return t('matchNegotiation.commentAdded'); - case 'update': - return t('matchNegotiation.matchUpdated'); - default: - return value || action; - } - }; + }, [match?.History, getHistoryTitle]); // Get allowed next statuses based on current status const allowedNextStatuses = useMemo(() => { @@ -325,7 +325,7 @@ const MatchNegotiationPage = () => { {t('matchNegotiation.co2Avoided')} - {match.EconomicImpact.co2_avoided_tonnes.toFixed(1)} t/year + {t('matchNegotiation.co2TonnesPerYear', { value: match.EconomicImpact.co2_avoided_tonnes.toFixed(1) })}
)} @@ -335,7 +335,7 @@ const MatchNegotiationPage = () => { {t('matchDetail.paybackPeriod')} - {match.EconomicImpact.payback_years.toFixed(1)} years + {t('matchDetail.paybackYears', { years: match.EconomicImpact.payback_years.toFixed(1) })}
)}