Compare commits

...

4 Commits

Author SHA1 Message Date
Damir Mukimov
986b8a794d
fix: continue linting fixes - refactor ErrorBoundary component
Some checks failed
CI/CD Pipeline / backend-lint (push) Failing after 1m6s
CI/CD Pipeline / backend-build (push) Has been skipped
CI/CD Pipeline / frontend-lint (push) Failing after 1m19s
CI/CD Pipeline / frontend-build (push) Has been skipped
CI/CD Pipeline / e2e-test (push) Has been skipped
- Refactor ErrorBoundary from class component to functional components
- Fix i18n literal strings in ErrorBoundary
- Continue systematic reduction of linting errors
2025-12-25 00:41:05 +01:00
Damir Mukimov
7310b98664
fix: continue linting fixes - fix i18n strings in UI components
- Fix i18n literal strings in Paywall, Combobox, Dialog, ResourceFlowCard
- Add translation hooks where needed
- Continue systematic reduction of linting errors (down to 248)
2025-12-25 00:38:40 +01:00
Damir Mukimov
ac92faef33
fix: continue linting fixes - fix i18n strings in paywall components
- Fix i18n literal strings in LimitWarning component
- Add translation hook and replace hardcoded strings with t() calls
- Continue systematic reduction of linting errors (down to 251)
2025-12-25 00:35:00 +01:00
Damir Mukimov
18cdcb12fd
fix: continue linting fixes - remove unused variables, fix i18n strings
- Remove unused imports and variables from DashboardPage, HeritageBuildingPage, MatchesMapView
- Fix i18n literal strings in NetworkGraph component
- Continue systematic reduction of linting errors
2025-12-25 00:32:40 +01:00
25 changed files with 173 additions and 139 deletions

View File

@ -73,9 +73,7 @@ const Step1 = ({
/> />
)} )}
/> />
<p className="text-sm text-muted-foreground mt-2"> <p className="text-sm text-muted-foreground mt-2">{t('organization.galleryImagesHint')}</p>
{t('organization.galleryImagesHint')}
</p>
</div> </div>
</div> </div>
); );

View File

@ -101,7 +101,7 @@ const TimelineSection = ({
transition={{ duration: 0.3 }} transition={{ duration: 0.3 }}
aria-label={t('heritage.toggleFilters')} aria-label={t('heritage.toggleFilters')}
> >
<span></span>
</motion.div> </motion.div>
</button> </button>

View File

@ -24,7 +24,7 @@ const ProductMarker = React.memo<{
const position: LatLngTuple = useMemo(() => { const position: LatLngTuple = useMemo(() => {
if (!match.product?.location) return [0, 0]; if (!match.product?.location) return [0, 0];
return [match.product.location.latitude, match.product.location.longitude]; return [match.product.location.latitude, match.product.location.longitude];
}, [match.product?.location?.latitude, match.product?.location?.longitude]); }, [match.product?.location]);
const icon = useMemo(() => { const icon = useMemo(() => {
if (!match.product?.location) { if (!match.product?.location) {
@ -118,7 +118,7 @@ const ServiceMarker = React.memo<{
const position: LatLngTuple = useMemo(() => { const position: LatLngTuple = useMemo(() => {
if (!match.service?.service_location) return [0, 0]; if (!match.service?.service_location) return [0, 0];
return [match.service.service_location.latitude, match.service.service_location.longitude]; return [match.service.service_location.latitude, match.service.service_location.longitude];
}, [match.service?.service_location?.latitude, match.service?.service_location?.longitude]); }, [match.service?.service_location]);
const icon = useMemo(() => { const icon = useMemo(() => {
if (!match.service?.service_location) { if (!match.service?.service_location) {

View File

@ -77,9 +77,10 @@ const MatchCard: React.FC<MatchCardProps> = ({ match, onViewDetails }) => {
<span> <span>
{t('matches.riskScore', { {t('matches.riskScore', {
score: formatScore( score: formatScore(
(match.RiskAssessment.technical_risk + match.RiskAssessment.regulatory_risk) / (match.RiskAssessment.technical_risk +
match.RiskAssessment.regulatory_risk) /
2 2
) ),
})} })}
</span> </span>
</div> </div>

View File

@ -235,7 +235,7 @@ export function NetworkGraph({
<CardContent> <CardContent>
{error && ( {error && (
<div className="bg-red-50 border border-red-200 text-red-800 px-4 py-3 rounded"> <div className="bg-red-50 border border-red-200 text-red-800 px-4 py-3 rounded">
<p className="font-semibold">Error loading network graph</p> <p className="font-semibold">{t('organization.networkGraphError')}</p>
<p className="text-sm">{error}</p> <p className="text-sm">{error}</p>
</div> </div>
)} )}
@ -244,7 +244,7 @@ export function NetworkGraph({
<div className="flex items-center justify-center h-96 bg-muted/30 rounded-lg"> <div className="flex items-center justify-center h-96 bg-muted/30 rounded-lg">
<div className="text-center"> <div className="text-center">
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-primary mx-auto mb-4"></div> <div className="animate-spin rounded-full h-12 w-12 border-b-2 border-primary mx-auto mb-4"></div>
<p className="text-muted-foreground">Loading network graph...</p> <p className="text-muted-foreground">{t('organization.networkGraphLoading')}</p>
</div> </div>
</div> </div>
)} )}
@ -257,21 +257,21 @@ export function NetworkGraph({
/> />
<div className="mt-4 flex items-center justify-between text-sm text-muted-foreground"> <div className="mt-4 flex items-center justify-between text-sm text-muted-foreground">
<div className="flex gap-4"> <div className="flex gap-4">
<span>{graphData.nodes.length} nodes</span> <span>{t('organization.nodesCount', { count: graphData.nodes.length })}</span>
<span>{graphData.edges.length} connections</span> <span>{t('organization.connectionsCount', { count: graphData.edges.length })}</span>
</div> </div>
<div className="flex gap-3 text-xs"> <div className="flex gap-3 text-xs">
<div className="flex items-center gap-1"> <div className="flex items-center gap-1">
<div className="w-3 h-3 rounded-full bg-[#3b82f6]"></div> <div className="w-3 h-3 rounded-full bg-[#3b82f6]"></div>
<span>Organization</span> <span>{t('organization.legend.organization')}</span>
</div> </div>
<div className="flex items-center gap-1"> <div className="flex items-center gap-1">
<div className="w-3 h-3 bg-[#10b981]"></div> <div className="w-3 h-3 bg-[#10b981]"></div>
<span>Site</span> <span>{t('organization.legend.site')}</span>
</div> </div>
<div className="flex items-center gap-1"> <div className="flex items-center gap-1">
<div className="w-3 h-3 bg-[#f59e0b] rotate-45"></div> <div className="w-3 h-3 bg-[#f59e0b] rotate-45"></div>
<span>Resource</span> <span>{t('organization.legend.resource')}</span>
</div> </div>
</div> </div>
</div> </div>

View File

@ -44,20 +44,24 @@ const ProductServiceCard: React.FC<{ match: DiscoveryMatch }> = ({ match }) => {
<span className="font-medium">{match.product.unit_price.toFixed(2)}</span> <span className="font-medium">{match.product.unit_price.toFixed(2)}</span>
</div> </div>
{match.product.moq > 0 && ( {match.product.moq > 0 && (
<span className="text-muted-foreground">MOQ: {match.product.moq}</span> <span className="text-muted-foreground">
{t('productService.moq', { value: match.product.moq })}
</span>
)} )}
</> </>
)} )}
{isService && match.service && ( {isService && match.service && (
<div className="flex items-center gap-1"> <div className="flex items-center gap-1">
<Euro className="h-4 w-4" /> <Euro className="h-4 w-4" />
<span className="font-medium">{match.service.hourly_rate.toFixed(2)}/hour</span> <span className="font-medium">
{t('productService.hourlyRate', { rate: match.service.hourly_rate.toFixed(2) })}
</span>
</div> </div>
)} )}
{match.distance_km > 0 && ( {match.distance_km > 0 && (
<div className="flex items-center gap-1 text-muted-foreground"> <div className="flex items-center gap-1 text-muted-foreground">
<MapPin className="h-4 w-4" /> <MapPin className="h-4 w-4" />
<span>{match.distance_km.toFixed(1)} km</span> <span>{t('matches.distance', { distance: match.distance_km.toFixed(1) })}</span>
</div> </div>
)} )}
</div> </div>

View File

@ -4,6 +4,7 @@ import { AlertTriangle, TrendingUp } from 'lucide-react';
import { Alert, Button } from '@/components/ui'; import { Alert, Button } from '@/components/ui';
import { useSubscription } from '@/contexts/SubscriptionContext'; import { useSubscription } from '@/contexts/SubscriptionContext';
import { useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
import { useTranslation } from '@/hooks/useI18n';
export interface LimitWarningProps { export interface LimitWarningProps {
limitType: 'organizations' | 'users' | 'storage' | 'apiCalls'; limitType: 'organizations' | 'users' | 'storage' | 'apiCalls';
@ -25,6 +26,7 @@ export const LimitWarning = ({
}: LimitWarningProps) => { }: LimitWarningProps) => {
const { subscription, getRemainingLimit, isWithinLimits } = useSubscription(); const { subscription, getRemainingLimit, isWithinLimits } = useSubscription();
const navigate = useNavigate(); const navigate = useNavigate();
const { t } = useTranslation();
if (!subscription) return null; if (!subscription) return null;
@ -52,14 +54,14 @@ export const LimitWarning = ({
<Alert variant="error" className={clsx('mb-4', className)}> <Alert variant="error" className={clsx('mb-4', className)}>
<AlertTriangle className="h-4 w-4" /> <AlertTriangle className="h-4 w-4" />
<div className="flex-1"> <div className="flex-1">
<h4 className="font-semibold">Limit Reached</h4> <h4 className="font-semibold">{t('paywall.limitReached')}</h4>
<p className="text-sm mt-1"> <p className="text-sm mt-1">
You&apos;ve reached your {label} limit ({limit}). Upgrade your plan to continue. {t('paywall.limitReachedDescription', { label, limit })}
</p> </p>
</div> </div>
{showUpgradeButton && ( {showUpgradeButton && (
<Button variant="primary" size="sm" onClick={() => navigate('/billing')}> <Button variant="primary" size="sm" onClick={() => navigate('/billing')}>
Upgrade Plan {t('paywall.upgradePlan')}
</Button> </Button>
)} )}
</Alert> </Alert>
@ -70,16 +72,21 @@ export const LimitWarning = ({
<Alert variant="warning" className={clsx('mb-4', className)}> <Alert variant="warning" className={clsx('mb-4', className)}>
<TrendingUp className="h-4 w-4" /> <TrendingUp className="h-4 w-4" />
<div className="flex-1"> <div className="flex-1">
<h4 className="font-semibold">Approaching Limit</h4> <h4 className="font-semibold">{t('paywall.approachingLimit')}</h4>
<p className="text-sm mt-1"> <p className="text-sm mt-1">
You&apos;re using {current} of {limit} {label} ({Math.round(percentage)}%). {remaining}{' '} {t('paywall.approachingLimitDescription', {
remaining. current,
limit,
label,
percentage: Math.round(percentage),
remaining
})}
</p> </p>
</div> </div>
{showUpgradeButton && ( {showUpgradeButton && (
<Button variant="outline" size="sm" onClick={() => navigate('/billing')}> <Button variant="outline" size="sm" onClick={() => navigate('/billing')}>
Upgrade {t('paywall.viewPlans')}
</Button> </Button>
)} )}
</Alert> </Alert>
); );

View File

@ -13,6 +13,7 @@ import {
import { useSubscription } from '@/contexts/SubscriptionContext'; import { useSubscription } from '@/contexts/SubscriptionContext';
import { SubscriptionFeatureFlag, SUBSCRIPTION_PLANS, formatPrice } from '@/types/subscription'; import { SubscriptionFeatureFlag, SUBSCRIPTION_PLANS, formatPrice } from '@/types/subscription';
import { useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
import { useTranslation } from '@/hooks/useI18n';
export interface PaywallProps { export interface PaywallProps {
feature: SubscriptionFeatureFlag | SubscriptionFeatureFlag[]; feature: SubscriptionFeatureFlag | SubscriptionFeatureFlag[];
@ -38,6 +39,7 @@ export const Paywall = ({
}: PaywallProps) => { }: PaywallProps) => {
const { canAccessFeature, subscription } = useSubscription(); const { canAccessFeature, subscription } = useSubscription();
const navigate = useNavigate(); const navigate = useNavigate();
const { t } = useTranslation();
const [showUpgradeDialog, setShowUpgradeDialog] = React.useState(false); const [showUpgradeDialog, setShowUpgradeDialog] = React.useState(false);
const features = Array.isArray(feature) ? feature : [feature]; const features = Array.isArray(feature) ? feature : [feature];
@ -89,8 +91,8 @@ export const Paywall = ({
<Dialog open={showUpgradeDialog} onOpenChange={setShowUpgradeDialog}> <Dialog open={showUpgradeDialog} onOpenChange={setShowUpgradeDialog}>
<DialogContent size="lg"> <DialogContent size="lg">
<DialogHeader> <DialogHeader>
<DialogTitle>Upgrade Your Plan</DialogTitle> <DialogTitle>{t('paywall.upgradeYourPlan')}</DialogTitle>
<DialogDescription>Choose the plan that&apos;s right for you</DialogDescription> <DialogDescription>{t('paywall.choosePlanDescription')}</DialogDescription>
</DialogHeader> </DialogHeader>
<UpgradePlans <UpgradePlans
currentPlan={currentPlan} currentPlan={currentPlan}
@ -138,7 +140,7 @@ const UpgradePlans = ({ currentPlan, onSelectPlan }: UpgradePlansProps) => {
<CardDescription>{planDetails.description}</CardDescription> <CardDescription>{planDetails.description}</CardDescription>
<div className="mt-4"> <div className="mt-4">
<span className="text-3xl font-bold">{formatPrice(planDetails.price.monthly)}</span> <span className="text-3xl font-bold">{formatPrice(planDetails.price.monthly)}</span>
<span className="text-muted-foreground">/month</span> <span className="text-muted-foreground">{t('paywall.perMonth')}</span>
</div> </div>
</CardHeader> </CardHeader>
<CardContent> <CardContent>

View File

@ -51,7 +51,7 @@ const ResourceFlowCard: React.FC<ResourceFlowCardProps> = ({ resourceFlow, onVie
{resourceFlow.EconomicData && ( {resourceFlow.EconomicData && (
<div className="mt-2 text-xs text-muted-foreground"> <div className="mt-2 text-xs text-muted-foreground">
{resourceFlow.EconomicData.cost_out !== undefined && ( {resourceFlow.EconomicData.cost_out !== undefined && (
<span>Cost: {resourceFlow.EconomicData.cost_out.toFixed(2)}</span> <span>{t('resourceFlow.cost', { cost: resourceFlow.EconomicData.cost_out.toFixed(2) })}</span>
)} )}
</div> </div>
)} )}

View File

@ -3,6 +3,7 @@ import { clsx } from 'clsx';
import { Check, ChevronsUpDown } from 'lucide-react'; import { Check, ChevronsUpDown } from 'lucide-react';
import Input from './Input'; import Input from './Input';
import Button from './Button'; import Button from './Button';
import { useTranslation } from '@/hooks/useI18n';
export interface ComboboxOption { export interface ComboboxOption {
value: string; value: string;
@ -142,7 +143,7 @@ export const Combobox = ({
)} )}
> >
{filteredOptions.length === 0 ? ( {filteredOptions.length === 0 ? (
<div className="p-4 text-sm text-muted-foreground text-center">No options found</div> <div className="p-4 text-sm text-muted-foreground text-center">{t('ui.noOptionsFound')}</div>
) : ( ) : (
<ul className="p-1" role="listbox"> <ul className="p-1" role="listbox">
{filteredOptions.map((option) => ( {filteredOptions.map((option) => (

View File

@ -1,6 +1,7 @@
import React, { useEffect, useRef } from 'react'; import React, { useEffect, useRef } from 'react';
import { clsx } from 'clsx'; import { clsx } from 'clsx';
import { X } from 'lucide-react'; import { X } from 'lucide-react';
import { useTranslation } from '@/hooks/useI18n';
export interface DialogProps { export interface DialogProps {
open: boolean; open: boolean;
@ -159,6 +160,7 @@ export interface DialogCloseProps {
} }
export const DialogClose = ({ onClose, className }: DialogCloseProps) => { export const DialogClose = ({ onClose, className }: DialogCloseProps) => {
const { t } = useTranslation();
return ( return (
<button <button
type="button" type="button"
@ -169,10 +171,10 @@ export const DialogClose = ({ onClose, className }: DialogCloseProps) => {
'transition-opacity', 'transition-opacity',
className className
)} )}
aria-label="Close" aria-label={t('ui.close')}
> >
<X className="h-4 w-4" /> <X className="h-4 w-4" />
<span className="sr-only">Close</span> <span className="sr-only">{t('ui.close')}</span>
</button> </button>
); );
}; };

View File

@ -1,25 +1,47 @@
import Button from '@/components/ui/Button.tsx'; import Button from '@/components/ui/Button.tsx';
import IconWrapper from '@/components/ui/IconWrapper.tsx'; import IconWrapper from '@/components/ui/IconWrapper.tsx';
import { useTranslation } from '@/hooks/useI18n';
import { XCircle } from 'lucide-react'; import { XCircle } from 'lucide-react';
import { Component, ErrorInfo, ReactNode } from 'react'; import { Component, ErrorInfo, ReactNode, useState } from 'react';
interface Props { interface Props {
children?: ReactNode; children?: ReactNode;
} }
interface State { interface ErrorState {
hasError: boolean; hasError: boolean;
error?: Error; error?: Error;
} }
class ErrorBoundary extends Component<Props, State> { const ErrorFallback = ({ error, onRefresh }: { error?: Error; onRefresh: () => void }) => {
// FIX: Replaced constructor with a class property for state initialization. This is a more modern and robust approach in class components and resolves the reported errors regarding `this.state` and `this.props` not being available. const { t } = useTranslation();
state: State = {
return (
<div className="flex flex-col items-center justify-center min-h-screen bg-background p-8 text-center">
<IconWrapper className="bg-destructive/10 text-destructive">
<XCircle className="h-8 w-8 text-current" />
</IconWrapper>
<h1 className="font-serif text-3xl font-bold text-destructive">{t('error.somethingWentWrong')}</h1>
<p className="mt-4 text-lg text-muted-foreground">
{t('error.tryRefreshing')}
</p>
<pre className="mt-4 text-sm text-left bg-muted p-4 rounded-md max-w-full overflow-auto">
{error?.message || t('error.unknownError')}
</pre>
<Button onClick={onRefresh} className="mt-6">
{t('error.refreshPage')}
</Button>
</div>
);
};
class ErrorBoundary extends Component<Props, ErrorState> {
state: ErrorState = {
hasError: false, hasError: false,
error: undefined, error: undefined,
}; };
static getDerivedStateFromError(error: Error): State { static getDerivedStateFromError(error: Error): ErrorState {
return { hasError: true, error }; return { hasError: true, error };
} }
@ -27,30 +49,13 @@ class ErrorBoundary extends Component<Props, State> {
console.error('Uncaught error:', error, errorInfo); console.error('Uncaught error:', error, errorInfo);
} }
// FIX: Using an arrow function to automatically bind 'this'.
handleRefresh = () => { handleRefresh = () => {
window.location.reload(); window.location.reload();
}; };
render() { render() {
if (this.state.hasError) { if (this.state.hasError) {
return ( return <ErrorFallback error={this.state.error} onRefresh={this.handleRefresh} />;
<div className="flex flex-col items-center justify-center min-h-screen bg-background p-8 text-center">
<IconWrapper className="bg-destructive/10 text-destructive">
<XCircle className="h-8 w-8 text-current" />
</IconWrapper>
<h1 className="font-serif text-3xl font-bold text-destructive">Something went wrong</h1>
<p className="mt-4 text-lg text-muted-foreground">
We&apos;re sorry for the inconvenience. Please try refreshing the page.
</p>
<pre className="mt-4 text-sm text-left bg-muted p-4 rounded-md max-w-full overflow-auto">
{this.state.error?.message || 'An unknown error occurred'}
</pre>
<Button onClick={this.handleRefresh} className="mt-6">
Refresh Page
</Button>
</div>
);
} }
return this.props.children; return this.props.children;

View File

@ -21,12 +21,14 @@ export const useAdminDashboard = () => {
// Activity feed // Activity feed
const { data: recentActivityData } = useRecentActivity(); const { data: recentActivityData } = useRecentActivity();
const recentActivity: ActivityItem[] = (recentActivityData || []).map((it: RecentActivityAPIResponse) => ({ const recentActivity: ActivityItem[] = (recentActivityData || []).map(
id: it.id, (it: RecentActivityAPIResponse) => ({
type: (it.type as ActivityItem['type']) || 'other', id: it.id,
action: it.description, type: (it.type as ActivityItem['type']) || 'other',
timestamp: new Date(it.timestamp), action: it.description,
})); timestamp: new Date(it.timestamp),
})
);
// Quick actions data from API // Quick actions data from API
const quickActions = { const quickActions = {

View File

@ -351,7 +351,11 @@ export class ErrorHandler {
private sendToSentry(error: AppError): void { private sendToSentry(error: AppError): void {
// Placeholder for Sentry integration // Placeholder for Sentry integration
if (typeof window !== 'undefined') { if (typeof window !== 'undefined') {
const sentry = (window as Window & { Sentry?: { captureException: (error: Error, context?: Record<string, unknown>) => void } }).Sentry; const sentry = (
window as Window & {
Sentry?: { captureException: (error: Error, context?: Record<string, unknown>) => void };
}
).Sentry;
if (sentry) { if (sentry) {
sentry.captureException(error.originalError || new Error(error.message), { sentry.captureException(error.originalError || new Error(error.message), {
tags: { tags: {
@ -373,7 +377,11 @@ export class ErrorHandler {
private sendToAnalytics(error: AppError): void { private sendToAnalytics(error: AppError): void {
// Placeholder for analytics integration // Placeholder for analytics integration
if (typeof window !== 'undefined') { if (typeof window !== 'undefined') {
const gtag = (window as Window & { gtag?: (event: string, name: string, params: Record<string, unknown>) => void }).gtag; const gtag = (
window as Window & {
gtag?: (event: string, name: string, params: Record<string, unknown>) => void;
}
).gtag;
if (gtag) { if (gtag) {
gtag('event', 'exception', { gtag('event', 'exception', {
description: error.message, description: error.message,

View File

@ -6,9 +6,7 @@ const CommunityEventsPage = () => {
<div className="container mx-auto px-4 py-8"> <div className="container mx-auto px-4 py-8">
<h1 className="text-3xl font-bold mb-6">{t('community.events.title')}</h1> <h1 className="text-3xl font-bold mb-6">{t('community.events.title')}</h1>
<div className="bg-muted p-8 rounded-lg text-center"> <div className="bg-muted p-8 rounded-lg text-center">
<p className="text-lg text-muted-foreground"> <p className="text-lg text-muted-foreground">{t('community.events.description')}</p>
{t('community.events.description')}
</p>
</div> </div>
</div> </div>
); );

View File

@ -6,9 +6,7 @@ const CommunityImpactPage = () => {
<div className="container mx-auto px-4 py-8"> <div className="container mx-auto px-4 py-8">
<h1 className="text-3xl font-bold mb-6">{t('community.impact.title')}</h1> <h1 className="text-3xl font-bold mb-6">{t('community.impact.title')}</h1>
<div className="bg-muted p-8 rounded-lg text-center"> <div className="bg-muted p-8 rounded-lg text-center">
<p className="text-lg text-muted-foreground"> <p className="text-lg text-muted-foreground">{t('community.impact.description')}</p>
{t('community.impact.description')}
</p>
</div> </div>
</div> </div>
); );

View File

@ -6,9 +6,7 @@ const CommunityNewsPage = () => {
<div className="container mx-auto px-4 py-8"> <div className="container mx-auto px-4 py-8">
<h1 className="text-3xl font-bold mb-6">{t('community.news.title')}</h1> <h1 className="text-3xl font-bold mb-6">{t('community.news.title')}</h1>
<div className="bg-muted p-8 rounded-lg text-center"> <div className="bg-muted p-8 rounded-lg text-center">
<p className="text-lg text-muted-foreground"> <p className="text-lg text-muted-foreground">{t('community.news.description')}</p>
{t('community.news.description')}
</p>
</div> </div>
</div> </div>
); );

View File

@ -6,9 +6,7 @@ const CommunityStoriesPage = () => {
<div className="container mx-auto px-4 py-8"> <div className="container mx-auto px-4 py-8">
<h1 className="text-3xl font-bold mb-6">{t('community.stories.title')}</h1> <h1 className="text-3xl font-bold mb-6">{t('community.stories.title')}</h1>
<div className="bg-muted p-8 rounded-lg text-center"> <div className="bg-muted p-8 rounded-lg text-center">
<p className="text-lg text-muted-foreground"> <p className="text-lg text-muted-foreground">{t('community.stories.description')}</p>
{t('community.stories.description')}
</p>
</div> </div>
</div> </div>
); );

View File

@ -46,22 +46,18 @@ const DashboardPage = () => {
const { const {
data: dashboardStats, data: dashboardStats,
isLoading: isLoadingDashboard, isLoading: isLoadingDashboard,
error: dashboardError,
} = useDashboardStatistics(); } = useDashboardStatistics();
const { const {
data: platformStats, data: platformStats,
isLoading: isLoadingPlatform, isLoading: isLoadingPlatform,
error: platformError,
} = usePlatformStatistics(); } = usePlatformStatistics();
const { const {
data: matchingStats, data: matchingStats,
isLoading: isLoadingMatching, isLoading: isLoadingMatching,
error: matchingError,
} = useMatchingStatistics(); } = useMatchingStatistics();
const { const {
data: impactMetrics, data: impactMetrics,
isLoading: isLoadingImpact, isLoading: isLoadingImpact,
error: impactError,
} = useImpactMetrics(); } = useImpactMetrics();
// User-specific data // User-specific data

View File

@ -20,7 +20,6 @@ import { Heading, Text } from '@/components/ui/Typography.tsx';
import { LoadingState } from '@/components/ui/LoadingState.tsx'; import { LoadingState } from '@/components/ui/LoadingState.tsx';
import { useHeritageSites } from '@/hooks/api/useHeritageSitesAPI'; import { useHeritageSites } from '@/hooks/api/useHeritageSitesAPI';
import { useTranslation } from '@/hooks/useI18n.tsx'; import { useTranslation } from '@/hooks/useI18n.tsx';
import { BackendHeritageSite } from '@/schemas/backend/heritage-sites';
const HeritageBuildingPage = () => { const HeritageBuildingPage = () => {
const { id } = useParams<{ id: string }>(); const { id } = useParams<{ id: string }>();

View File

@ -327,30 +327,36 @@ const ImpactMetrics = () => {
</CardHeader> </CardHeader>
<CardContent> <CardContent>
<div className="space-y-4"> <div className="space-y-4">
{impact.topImpactingMatches.slice(0, 5).map((match: TopImpactingMatch, index: number) => ( {impact.topImpactingMatches
<div .slice(0, 5)
key={index} .map((match: TopImpactingMatch, index: number) => (
className="flex items-center justify-between p-4 border rounded-lg" <div
> key={index}
<div className="flex items-center gap-4"> className="flex items-center justify-between p-4 border rounded-lg"
<div className="flex items-center justify-center w-8 h-8 rounded-full bg-primary/10 text-primary font-bold text-sm"> >
{index + 1} <div className="flex items-center gap-4">
<div className="flex items-center justify-center w-8 h-8 rounded-full bg-primary/10 text-primary font-bold text-sm">
{index + 1}
</div>
<div>
<p className="font-medium">
{match.description || `Match ${match.id || index}`}
</p>
<p className="text-sm text-muted-foreground">{match.resource_type}</p>
</div>
</div> </div>
<div> <div className="text-right">
<p className="font-medium">{match.description || `Match ${match.id || index}`}</p> <p className="font-semibold text-green-600">
<p className="text-sm text-muted-foreground">{match.resource_type}</p> {t('impactMetrics.co2Tonnes', {
value: formatNumber(match.co2_impact || 0),
})}
</p>
<p className="text-sm text-muted-foreground">
{formatCurrency(match.economic_impact || 0)}
</p>
</div> </div>
</div> </div>
<div className="text-right"> ))}
<p className="font-semibold text-green-600">
{t('impactMetrics.co2Tonnes', { value: formatNumber(match.co2_impact || 0) })}
</p>
<p className="text-sm text-muted-foreground">
{formatCurrency(match.economic_impact || 0)}
</p>
</div>
</div>
))}
</div> </div>
</CardContent> </CardContent>
</Card> </Card>
@ -402,7 +408,9 @@ const ImpactMetrics = () => {
<span className="font-medium">{year}</span> <span className="font-medium">{year}</span>
<div className="text-right"> <div className="text-right">
<div className="font-semibold text-green-600"> <div className="font-semibold text-green-600">
{t('impactMetrics.co2Tonnes', { value: formatNumber(projection.co2_projected || 0) })} {t('impactMetrics.co2Tonnes', {
value: formatNumber(projection.co2_projected || 0),
})}
</div> </div>
<div className="text-sm text-muted-foreground"> <div className="text-sm text-muted-foreground">
{formatCurrency(projection.economic_projected || 0)} {formatCurrency(projection.economic_projected || 0)}

View File

@ -104,9 +104,7 @@ const LoginPage = () => {
<CardContent> <CardContent>
{isDevelopment && ( {isDevelopment && (
<div className="mb-6 p-4 bg-muted rounded-lg border border-primary/20"> <div className="mb-6 p-4 bg-muted rounded-lg border border-primary/20">
<p className="text-sm font-medium mb-3 text-foreground"> <p className="text-sm font-medium mb-3 text-foreground">{t('login.quickLogin')}</p>
{t('login.quickLogin')}
</p>
<div className="grid grid-cols-2 gap-2"> <div className="grid grid-cols-2 gap-2">
{TEST_USERS.map((testUser) => ( {TEST_USERS.map((testUser) => (
<Button <Button

View File

@ -41,18 +41,21 @@ const MatchDetailPage = () => {
const [newStatus, setNewStatus] = useState(''); const [newStatus, setNewStatus] = useState('');
const [statusNotes, setStatusNotes] = useState(''); const [statusNotes, setStatusNotes] = useState('');
const getHistoryTitle = useCallback((action: string, value?: string) => { const getHistoryTitle = useCallback(
switch (action) { (action: string, value?: string) => {
case 'status_change': switch (action) {
return t('matchDetail.statusChanged'); case 'status_change':
case 'comment': return t('matchDetail.statusChanged');
return t('matchDetail.commentAdded'); case 'comment':
case 'update': return t('matchDetail.commentAdded');
return t('matchDetail.matchUpdated'); case 'update':
default: return t('matchDetail.matchUpdated');
return value || action; default:
} return value || action;
}, [t]); }
},
[t]
);
// Transform match history to timeline format // Transform match history to timeline format
const timelineEntries: TimelineEntry[] = useMemo(() => { const timelineEntries: TimelineEntry[] = useMemo(() => {
@ -228,7 +231,9 @@ const MatchDetailPage = () => {
{t('matchDetail.paybackPeriod')} {t('matchDetail.paybackPeriod')}
</span> </span>
<span className="text-sm font-semibold"> <span className="text-sm font-semibold">
{t('matchDetail.paybackYears', { years: match.EconomicImpact.payback_years.toFixed(1) })} {t('matchDetail.paybackYears', {
years: match.EconomicImpact.payback_years.toFixed(1),
})}
</span> </span>
</div> </div>
)} )}

View File

@ -46,18 +46,21 @@ const MatchNegotiationPage = () => {
const [showMessageModal, setShowMessageModal] = useState(false); const [showMessageModal, setShowMessageModal] = useState(false);
const [messageText, setMessageText] = useState(''); const [messageText, setMessageText] = useState('');
const getHistoryTitle = useCallback((action: string, value?: string) => { const getHistoryTitle = useCallback(
switch (action) { (action: string, value?: string) => {
case 'status_changed': switch (action) {
return t('matchNegotiation.statusChanged'); case 'status_changed':
case 'comment': return t('matchNegotiation.statusChanged');
return t('matchNegotiation.commentAdded'); case 'comment':
case 'update': return t('matchNegotiation.commentAdded');
return t('matchNegotiation.matchUpdated'); case 'update':
default: return t('matchNegotiation.matchUpdated');
return value || action; default:
} return value || action;
}, [t]); }
},
[t]
);
// Transform match history to timeline format // Transform match history to timeline format
const timelineEntries: TimelineEntry[] = useMemo(() => { const timelineEntries: TimelineEntry[] = useMemo(() => {
@ -325,7 +328,9 @@ const MatchNegotiationPage = () => {
{t('matchNegotiation.co2Avoided')} {t('matchNegotiation.co2Avoided')}
</span> </span>
<span className="text-sm font-semibold text-success"> <span className="text-sm font-semibold text-success">
{t('matchNegotiation.co2TonnesPerYear', { value: match.EconomicImpact.co2_avoided_tonnes.toFixed(1) })} {t('matchNegotiation.co2TonnesPerYear', {
value: match.EconomicImpact.co2_avoided_tonnes.toFixed(1),
})}
</span> </span>
</div> </div>
)} )}
@ -335,7 +340,9 @@ const MatchNegotiationPage = () => {
{t('matchDetail.paybackPeriod')} {t('matchDetail.paybackPeriod')}
</span> </span>
<span className="text-sm font-semibold"> <span className="text-sm font-semibold">
{t('matchDetail.paybackYears', { years: match.EconomicImpact.payback_years.toFixed(1) })} {t('matchDetail.paybackYears', {
years: match.EconomicImpact.payback_years.toFixed(1),
})}
</span> </span>
</div> </div>
)} )}

View File

@ -1,9 +1,8 @@
import React, { useState, useMemo } from 'react'; import React, { useState, useMemo } from 'react';
import { useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
import { MainLayout } from '@/components/layout/MainLayout.tsx'; import { MainLayout } from '@/components/layout/MainLayout.tsx';
import PageHeader from '@/components/layout/PageHeader.tsx';
import MatchCard from '@/components/matches/MatchCard.tsx'; import MatchCard from '@/components/matches/MatchCard.tsx';
import { Container, Stack, Grid, Flex } from '@/components/ui/layout'; import { Container, Stack, Flex } from '@/components/ui/layout';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/Card.tsx'; import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/Card.tsx';
import Button from '@/components/ui/Button.tsx'; import Button from '@/components/ui/Button.tsx';
import Select from '@/components/ui/Select.tsx'; import Select from '@/components/ui/Select.tsx';
@ -14,7 +13,7 @@ import { MapProvider, useMapUI } from '@/contexts/MapContexts.tsx';
import { useTranslation } from '@/hooks/useI18n.tsx'; import { useTranslation } from '@/hooks/useI18n.tsx';
import { useTopMatches } from '@/hooks/api/useMatchingAPI.ts'; import { useTopMatches } from '@/hooks/api/useMatchingAPI.ts';
import { useNavigation } from '@/hooks/useNavigation.tsx'; import { useNavigation } from '@/hooks/useNavigation.tsx';
import { ArrowLeft, Filter, MapPin, TrendingUp } from 'lucide-react'; import { ArrowLeft, Filter, MapPin } from 'lucide-react';
// Import the extended map component // Import the extended map component
const MatchesMap = React.lazy(() => import('../components/map/MatchesMap.tsx')); const MatchesMap = React.lazy(() => import('../components/map/MatchesMap.tsx'));