mirror of
https://github.com/SamyRai/turash.git
synced 2025-12-26 23:01:33 +00:00
Compare commits
No commits in common. "986b8a794df7db910b81e11ecab78a049097a6e8" and "f24628a24857c3ce46d949e74a90ca09af617647" have entirely different histories.
986b8a794d
...
f24628a248
@ -73,7 +73,9 @@ const Step1 = ({
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
<p className="text-sm text-muted-foreground mt-2">{t('organization.galleryImagesHint')}</p>
|
<p className="text-sm text-muted-foreground mt-2">
|
||||||
|
{t('organization.galleryImagesHint')}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -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>
|
||||||
|
|
||||||
|
|||||||
@ -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]);
|
}, [match.product?.location?.latitude, match.product?.location?.longitude]);
|
||||||
|
|
||||||
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]);
|
}, [match.service?.service_location?.latitude, match.service?.service_location?.longitude]);
|
||||||
|
|
||||||
const icon = useMemo(() => {
|
const icon = useMemo(() => {
|
||||||
if (!match.service?.service_location) {
|
if (!match.service?.service_location) {
|
||||||
|
|||||||
@ -77,10 +77,9 @@ 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.technical_risk + match.RiskAssessment.regulatory_risk) /
|
||||||
match.RiskAssessment.regulatory_risk) /
|
|
||||||
2
|
2
|
||||||
),
|
)
|
||||||
})}
|
})}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -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">{t('organization.networkGraphError')}</p>
|
<p className="font-semibold">Error loading network graph</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">{t('organization.networkGraphLoading')}</p>
|
<p className="text-muted-foreground">Loading network graph...</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>{t('organization.nodesCount', { count: graphData.nodes.length })}</span>
|
<span>{graphData.nodes.length} nodes</span>
|
||||||
<span>{t('organization.connectionsCount', { count: graphData.edges.length })}</span>
|
<span>{graphData.edges.length} connections</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>{t('organization.legend.organization')}</span>
|
<span>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>{t('organization.legend.site')}</span>
|
<span>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>{t('organization.legend.resource')}</span>
|
<span>Resource</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -44,24 +44,20 @@ 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">
|
<span className="text-muted-foreground">MOQ: {match.product.moq}</span>
|
||||||
{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">
|
<span className="font-medium">€{match.service.hourly_rate.toFixed(2)}/hour</span>
|
||||||
{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>{t('matches.distance', { distance: match.distance_km.toFixed(1) })}</span>
|
<span>{match.distance_km.toFixed(1)} km</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -4,7 +4,6 @@ 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';
|
||||||
@ -26,7 +25,6 @@ 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;
|
||||||
|
|
||||||
@ -54,14 +52,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">{t('paywall.limitReached')}</h4>
|
<h4 className="font-semibold">Limit Reached</h4>
|
||||||
<p className="text-sm mt-1">
|
<p className="text-sm mt-1">
|
||||||
{t('paywall.limitReachedDescription', { label, limit })}
|
You've reached your {label} limit ({limit}). Upgrade your plan to continue.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
{showUpgradeButton && (
|
{showUpgradeButton && (
|
||||||
<Button variant="primary" size="sm" onClick={() => navigate('/billing')}>
|
<Button variant="primary" size="sm" onClick={() => navigate('/billing')}>
|
||||||
{t('paywall.upgradePlan')}
|
Upgrade Plan
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
</Alert>
|
</Alert>
|
||||||
@ -72,21 +70,16 @@ 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">{t('paywall.approachingLimit')}</h4>
|
<h4 className="font-semibold">Approaching Limit</h4>
|
||||||
<p className="text-sm mt-1">
|
<p className="text-sm mt-1">
|
||||||
{t('paywall.approachingLimitDescription', {
|
You're using {current} of {limit} {label} ({Math.round(percentage)}%). {remaining}{' '}
|
||||||
current,
|
remaining.
|
||||||
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')}>
|
||||||
{t('paywall.viewPlans')}
|
Upgrade
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
</Alert>
|
</Alert>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -13,7 +13,6 @@ 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[];
|
||||||
@ -39,7 +38,6 @@ 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];
|
||||||
@ -91,8 +89,8 @@ export const Paywall = ({
|
|||||||
<Dialog open={showUpgradeDialog} onOpenChange={setShowUpgradeDialog}>
|
<Dialog open={showUpgradeDialog} onOpenChange={setShowUpgradeDialog}>
|
||||||
<DialogContent size="lg">
|
<DialogContent size="lg">
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle>{t('paywall.upgradeYourPlan')}</DialogTitle>
|
<DialogTitle>Upgrade Your Plan</DialogTitle>
|
||||||
<DialogDescription>{t('paywall.choosePlanDescription')}</DialogDescription>
|
<DialogDescription>Choose the plan that's right for you</DialogDescription>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
<UpgradePlans
|
<UpgradePlans
|
||||||
currentPlan={currentPlan}
|
currentPlan={currentPlan}
|
||||||
@ -140,7 +138,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">{t('paywall.perMonth')}</span>
|
<span className="text-muted-foreground">/month</span>
|
||||||
</div>
|
</div>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
|
|||||||
@ -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>{t('resourceFlow.cost', { cost: resourceFlow.EconomicData.cost_out.toFixed(2) })}</span>
|
<span>Cost: €{resourceFlow.EconomicData.cost_out.toFixed(2)}</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@ -3,7 +3,6 @@ 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;
|
||||||
@ -143,7 +142,7 @@ export const Combobox = ({
|
|||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{filteredOptions.length === 0 ? (
|
{filteredOptions.length === 0 ? (
|
||||||
<div className="p-4 text-sm text-muted-foreground text-center">{t('ui.noOptionsFound')}</div>
|
<div className="p-4 text-sm text-muted-foreground text-center">No options found</div>
|
||||||
) : (
|
) : (
|
||||||
<ul className="p-1" role="listbox">
|
<ul className="p-1" role="listbox">
|
||||||
{filteredOptions.map((option) => (
|
{filteredOptions.map((option) => (
|
||||||
|
|||||||
@ -1,7 +1,6 @@
|
|||||||
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;
|
||||||
@ -160,7 +159,6 @@ 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"
|
||||||
@ -171,10 +169,10 @@ export const DialogClose = ({ onClose, className }: DialogCloseProps) => {
|
|||||||
'transition-opacity',
|
'transition-opacity',
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
aria-label={t('ui.close')}
|
aria-label="Close"
|
||||||
>
|
>
|
||||||
<X className="h-4 w-4" />
|
<X className="h-4 w-4" />
|
||||||
<span className="sr-only">{t('ui.close')}</span>
|
<span className="sr-only">Close</span>
|
||||||
</button>
|
</button>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -1,47 +1,25 @@
|
|||||||
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, useState } from 'react';
|
import { Component, ErrorInfo, ReactNode } from 'react';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
children?: ReactNode;
|
children?: ReactNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ErrorState {
|
interface State {
|
||||||
hasError: boolean;
|
hasError: boolean;
|
||||||
error?: Error;
|
error?: Error;
|
||||||
}
|
}
|
||||||
|
|
||||||
const ErrorFallback = ({ error, onRefresh }: { error?: Error; onRefresh: () => void }) => {
|
class ErrorBoundary extends Component<Props, State> {
|
||||||
const { t } = useTranslation();
|
// 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.
|
||||||
|
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): ErrorState {
|
static getDerivedStateFromError(error: Error): State {
|
||||||
return { hasError: true, error };
|
return { hasError: true, error };
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -49,13 +27,30 @@ class ErrorBoundary extends Component<Props, ErrorState> {
|
|||||||
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 <ErrorFallback error={this.state.error} onRefresh={this.handleRefresh} />;
|
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">Something went wrong</h1>
|
||||||
|
<p className="mt-4 text-lg text-muted-foreground">
|
||||||
|
We'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;
|
||||||
|
|||||||
@ -21,14 +21,12 @@ export const useAdminDashboard = () => {
|
|||||||
|
|
||||||
// Activity feed
|
// Activity feed
|
||||||
const { data: recentActivityData } = useRecentActivity();
|
const { data: recentActivityData } = useRecentActivity();
|
||||||
const recentActivity: ActivityItem[] = (recentActivityData || []).map(
|
const recentActivity: ActivityItem[] = (recentActivityData || []).map((it: RecentActivityAPIResponse) => ({
|
||||||
(it: RecentActivityAPIResponse) => ({
|
id: it.id,
|
||||||
id: it.id,
|
type: (it.type as ActivityItem['type']) || 'other',
|
||||||
type: (it.type as ActivityItem['type']) || 'other',
|
action: it.description,
|
||||||
action: it.description,
|
timestamp: new Date(it.timestamp),
|
||||||
timestamp: new Date(it.timestamp),
|
}));
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
// Quick actions data from API
|
// Quick actions data from API
|
||||||
const quickActions = {
|
const quickActions = {
|
||||||
|
|||||||
@ -351,11 +351,7 @@ 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 = (
|
const sentry = (window as Window & { Sentry?: { captureException: (error: Error, context?: Record<string, unknown>) => void } }).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: {
|
||||||
@ -377,11 +373,7 @@ 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 = (
|
const gtag = (window as Window & { gtag?: (event: string, name: string, params: Record<string, unknown>) => void }).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,
|
||||||
|
|||||||
@ -6,7 +6,9 @@ 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">{t('community.events.description')}</p>
|
<p className="text-lg text-muted-foreground">
|
||||||
|
{t('community.events.description')}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -6,7 +6,9 @@ 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">{t('community.impact.description')}</p>
|
<p className="text-lg text-muted-foreground">
|
||||||
|
{t('community.impact.description')}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -6,7 +6,9 @@ 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">{t('community.news.description')}</p>
|
<p className="text-lg text-muted-foreground">
|
||||||
|
{t('community.news.description')}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -6,7 +6,9 @@ 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">{t('community.stories.description')}</p>
|
<p className="text-lg text-muted-foreground">
|
||||||
|
{t('community.stories.description')}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -46,18 +46,22 @@ 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
|
||||||
|
|||||||
@ -20,13 +20,14 @@ 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 }>();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const { data: heritageSites, isLoading } = useHeritageSites();
|
const { data: heritageSites, isLoading } = useHeritageSites();
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
const building = useMemo(() => {
|
const building = useMemo(() => {
|
||||||
if (heritageSites && id) {
|
if (heritageSites && id) {
|
||||||
return heritageSites.find((site) => String(site.ID) === id) || null;
|
return heritageSites.find((site) => String(site.ID) === id) || null;
|
||||||
|
|||||||
@ -327,36 +327,30 @@ const ImpactMetrics = () => {
|
|||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
{impact.topImpactingMatches
|
{impact.topImpactingMatches.slice(0, 5).map((match: TopImpactingMatch, index: number) => (
|
||||||
.slice(0, 5)
|
<div
|
||||||
.map((match: TopImpactingMatch, index: number) => (
|
key={index}
|
||||||
<div
|
className="flex items-center justify-between p-4 border rounded-lg"
|
||||||
key={index}
|
>
|
||||||
className="flex items-center justify-between p-4 border rounded-lg"
|
<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">
|
||||||
<div className="flex items-center gap-4">
|
{index + 1}
|
||||||
<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 className="text-right">
|
<div>
|
||||||
<p className="font-semibold text-green-600">
|
<p className="font-medium">{match.description || `Match ${match.id || index}`}</p>
|
||||||
{t('impactMetrics.co2Tonnes', {
|
<p className="text-sm text-muted-foreground">{match.resource_type}</p>
|
||||||
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>
|
||||||
@ -408,9 +402,7 @@ 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', {
|
{t('impactMetrics.co2Tonnes', { value: formatNumber(projection.co2_projected || 0) })}
|
||||||
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)}
|
||||||
|
|||||||
@ -104,7 +104,9 @@ 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">{t('login.quickLogin')}</p>
|
<p className="text-sm font-medium mb-3 text-foreground">
|
||||||
|
{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
|
||||||
|
|||||||
@ -41,21 +41,18 @@ const MatchDetailPage = () => {
|
|||||||
const [newStatus, setNewStatus] = useState('');
|
const [newStatus, setNewStatus] = useState('');
|
||||||
const [statusNotes, setStatusNotes] = useState('');
|
const [statusNotes, setStatusNotes] = useState('');
|
||||||
|
|
||||||
const getHistoryTitle = useCallback(
|
const getHistoryTitle = useCallback((action: string, value?: string) => {
|
||||||
(action: string, value?: string) => {
|
switch (action) {
|
||||||
switch (action) {
|
case 'status_change':
|
||||||
case 'status_change':
|
return t('matchDetail.statusChanged');
|
||||||
return t('matchDetail.statusChanged');
|
case 'comment':
|
||||||
case 'comment':
|
return t('matchDetail.commentAdded');
|
||||||
return t('matchDetail.commentAdded');
|
case 'update':
|
||||||
case 'update':
|
return t('matchDetail.matchUpdated');
|
||||||
return t('matchDetail.matchUpdated');
|
default:
|
||||||
default:
|
return value || action;
|
||||||
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(() => {
|
||||||
@ -231,9 +228,7 @@ 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', {
|
{t('matchDetail.paybackYears', { years: match.EconomicImpact.payback_years.toFixed(1) })}
|
||||||
years: match.EconomicImpact.payback_years.toFixed(1),
|
|
||||||
})}
|
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@ -46,21 +46,18 @@ const MatchNegotiationPage = () => {
|
|||||||
const [showMessageModal, setShowMessageModal] = useState(false);
|
const [showMessageModal, setShowMessageModal] = useState(false);
|
||||||
const [messageText, setMessageText] = useState('');
|
const [messageText, setMessageText] = useState('');
|
||||||
|
|
||||||
const getHistoryTitle = useCallback(
|
const getHistoryTitle = useCallback((action: string, value?: string) => {
|
||||||
(action: string, value?: string) => {
|
switch (action) {
|
||||||
switch (action) {
|
case 'status_changed':
|
||||||
case 'status_changed':
|
return t('matchNegotiation.statusChanged');
|
||||||
return t('matchNegotiation.statusChanged');
|
case 'comment':
|
||||||
case 'comment':
|
return t('matchNegotiation.commentAdded');
|
||||||
return t('matchNegotiation.commentAdded');
|
case 'update':
|
||||||
case 'update':
|
return t('matchNegotiation.matchUpdated');
|
||||||
return t('matchNegotiation.matchUpdated');
|
default:
|
||||||
default:
|
return value || action;
|
||||||
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(() => {
|
||||||
@ -328,9 +325,7 @@ 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', {
|
{t('matchNegotiation.co2TonnesPerYear', { value: match.EconomicImpact.co2_avoided_tonnes.toFixed(1) })}
|
||||||
value: match.EconomicImpact.co2_avoided_tonnes.toFixed(1),
|
|
||||||
})}
|
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@ -340,9 +335,7 @@ 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', {
|
{t('matchDetail.paybackYears', { years: match.EconomicImpact.payback_years.toFixed(1) })}
|
||||||
years: match.EconomicImpact.payback_years.toFixed(1),
|
|
||||||
})}
|
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@ -1,8 +1,9 @@
|
|||||||
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, Flex } from '@/components/ui/layout';
|
import { Container, Stack, Grid, 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';
|
||||||
@ -13,7 +14,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 } from 'lucide-react';
|
import { ArrowLeft, Filter, MapPin, TrendingUp } 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'));
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user