mirror of
https://github.com/SamyRai/turash.git
synced 2025-12-26 23:01:33 +00:00
- Remove nested git repository from bugulma/frontend/.git - Add all frontend files to main repository tracking - Convert from separate frontend/backend repos to unified monorepo - Preserve all frontend code and development history as tracked files - Eliminate nested repository complexity for simpler development workflow This creates a proper monorepo structure with frontend and backend coexisting in the same repository for easier development and deployment.
339 lines
14 KiB
TypeScript
339 lines
14 KiB
TypeScript
import React, { useState, useMemo } from 'react';
|
|
import { useNavigate } from 'react-router-dom';
|
|
import { MainLayout } from '@/components/layout/MainLayout.tsx';
|
|
import PageHeader from '@/components/layout/PageHeader.tsx';
|
|
import MatchCard from '@/components/matches/MatchCard.tsx';
|
|
import { Container, Stack, Grid, Flex } from '@/components/ui/layout';
|
|
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/Card.tsx';
|
|
import Button from '@/components/ui/Button.tsx';
|
|
import Select from '@/components/ui/Select.tsx';
|
|
import Input from '@/components/ui/Input.tsx';
|
|
import Badge from '@/components/ui/Badge.tsx';
|
|
import ModuleErrorBoundary from '@/components/ui/ModuleErrorBoundary.tsx';
|
|
import { MapProvider, useMapUI } from '@/contexts/MapContexts.tsx';
|
|
import { useTranslation } from '@/hooks/useI18n.tsx';
|
|
import { useTopMatches } from '@/hooks/api/useMatchingAPI.ts';
|
|
import { useNavigation } from '@/hooks/useNavigation.tsx';
|
|
import { ArrowLeft, Filter, MapPin, TrendingUp } from 'lucide-react';
|
|
|
|
// Import the extended map component
|
|
const MatchesMap = React.lazy(() => import('../components/map/MatchesMap.tsx'));
|
|
|
|
interface MatchesMapContentProps {}
|
|
|
|
const MatchesMapContent: React.FC<MatchesMapContentProps> = () => {
|
|
const { t } = useTranslation();
|
|
const navigate = useNavigate();
|
|
const { handleBackNavigation, handleFooterNavigate } = useNavigation();
|
|
const { isSidebarOpen, setIsSidebarOpen } = useMapUI();
|
|
|
|
// Match filtering state
|
|
const [selectedMatch, setSelectedMatch] = useState<string | null>(null);
|
|
const [statusFilter, setStatusFilter] = useState<string>('all');
|
|
const [minScoreFilter, setMinScoreFilter] = useState<number>(0.5);
|
|
const [maxDistanceFilter, setMaxDistanceFilter] = useState<number>(50);
|
|
|
|
// Get matches data
|
|
const { data: matchesData, isLoading } = useTopMatches(100);
|
|
|
|
// Filter matches based on criteria
|
|
const filteredMatches = useMemo(() => {
|
|
if (!matchesData?.matches) return [];
|
|
|
|
return matchesData.matches.filter(match => {
|
|
// Status filter
|
|
if (statusFilter !== 'all' && match.Status !== statusFilter) {
|
|
return false;
|
|
}
|
|
|
|
// Score filter
|
|
if (match.CompatibilityScore < minScoreFilter) {
|
|
return false;
|
|
}
|
|
|
|
// Distance filter
|
|
if (match.DistanceKm > maxDistanceFilter) {
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
});
|
|
}, [matchesData, statusFilter, minScoreFilter, maxDistanceFilter]);
|
|
|
|
const statusOptions = [
|
|
{ value: 'all', label: t('matchesMap.allStatuses', 'All Statuses') },
|
|
{ value: 'suggested', label: t('matchStatus.suggested', 'Suggested') },
|
|
{ value: 'negotiating', label: t('matchStatus.negotiating', 'Negotiating') },
|
|
{ value: 'reserved', label: t('matchStatus.reserved', 'Reserved') },
|
|
{ value: 'contracted', label: t('matchStatus.contracted', 'Contracted') },
|
|
{ value: 'live', label: t('matchStatus.live', 'Live') },
|
|
];
|
|
|
|
const selectedMatchData = useMemo(() => {
|
|
return selectedMatch ? filteredMatches.find(m => m.ID === selectedMatch) : null;
|
|
}, [selectedMatch, filteredMatches]);
|
|
|
|
const handleViewMatchDetails = (matchId: string) => {
|
|
navigate(`/matching/${matchId}`);
|
|
};
|
|
|
|
const handleMatchSelect = (matchId: string) => {
|
|
setSelectedMatch(matchId === selectedMatch ? null : matchId);
|
|
};
|
|
|
|
return (
|
|
<MainLayout onNavigate={handleFooterNavigate} className="bg-muted/30">
|
|
<Container size="full" className="py-0">
|
|
<div className="h-screen w-screen flex flex-col">
|
|
{/* Header */}
|
|
<div className="bg-background border-b px-4 py-3 flex items-center justify-between shrink-0">
|
|
<Flex align="center" gap="md">
|
|
<Button
|
|
variant="ghost"
|
|
size="sm"
|
|
onClick={handleBackNavigation}
|
|
className="p-2"
|
|
>
|
|
<ArrowLeft className="h-4 text-current w-4" />
|
|
</Button>
|
|
<div>
|
|
<h1 className="text-lg font-semibold">{t('matchesMap.title', 'Matches Map')}</h1>
|
|
<p className="text-sm text-muted-foreground">
|
|
{t('matchesMap.subtitle', 'Visualize resource flow matches on the map')}
|
|
</p>
|
|
</div>
|
|
</Flex>
|
|
|
|
{/* Quick stats */}
|
|
<Flex gap="md" className="hidden sm:flex">
|
|
<Badge variant="outline">
|
|
{filteredMatches.length} {t('matchesMap.matches', 'matches')}
|
|
</Badge>
|
|
<Badge variant="outline">
|
|
{filteredMatches.filter(m => m.Status === 'live').length} {t('matchesMap.live', 'live')}
|
|
</Badge>
|
|
</Flex>
|
|
</div>
|
|
|
|
<div className="flex-1 flex overflow-hidden">
|
|
{/* Sidebar */}
|
|
<div
|
|
className={`bg-background border-r transition-all duration-300 ${
|
|
isSidebarOpen ? 'w-80' : 'w-0'
|
|
} overflow-hidden`}
|
|
>
|
|
<div className="p-4 h-full overflow-y-auto">
|
|
<Stack spacing="md">
|
|
{/* Filters */}
|
|
<Card>
|
|
<CardHeader className="pb-3">
|
|
<CardTitle className="flex items-center gap-2 text-base">
|
|
<Filter className="h-4 text-current w-4" />
|
|
{t('matchesMap.filters', 'Filters')}
|
|
</CardTitle>
|
|
</CardHeader>
|
|
<CardContent className="space-y-4">
|
|
<div>
|
|
<label className="block text-sm font-medium mb-2">
|
|
{t('matchesMap.status', 'Status')}
|
|
</label>
|
|
<Select
|
|
value={statusFilter}
|
|
onValueChange={setStatusFilter}
|
|
options={statusOptions}
|
|
/>
|
|
</div>
|
|
|
|
<div>
|
|
<label className="block text-sm font-medium mb-2">
|
|
{t('matchesMap.minScore', 'Min Compatibility Score')}
|
|
</label>
|
|
<Input
|
|
type="range"
|
|
min="0"
|
|
max="1"
|
|
step="0.1"
|
|
value={minScoreFilter}
|
|
onChange={(e) => setMinScoreFilter(Number(e.target.value))}
|
|
className="w-full"
|
|
/>
|
|
<div className="text-xs text-muted-foreground mt-1">
|
|
{Math.round(minScoreFilter * 100)}%
|
|
</div>
|
|
</div>
|
|
|
|
<div>
|
|
<label className="block text-sm font-medium mb-2">
|
|
{t('matchesMap.maxDistance', 'Max Distance (km)')}
|
|
</label>
|
|
<Input
|
|
type="range"
|
|
min="1"
|
|
max="100"
|
|
step="5"
|
|
value={maxDistanceFilter}
|
|
onChange={(e) => setMaxDistanceFilter(Number(e.target.value))}
|
|
className="w-full"
|
|
/>
|
|
<div className="text-xs text-muted-foreground mt-1">
|
|
{maxDistanceFilter} km
|
|
</div>
|
|
</div>
|
|
|
|
<Button
|
|
variant="outline"
|
|
size="sm"
|
|
onClick={() => {
|
|
setStatusFilter('all');
|
|
setMinScoreFilter(0.5);
|
|
setMaxDistanceFilter(50);
|
|
}}
|
|
className="w-full"
|
|
>
|
|
{t('common.reset', 'Reset Filters')}
|
|
</Button>
|
|
</CardContent>
|
|
</Card>
|
|
|
|
{/* Selected Match Details */}
|
|
{selectedMatchData && (
|
|
<Card>
|
|
<CardHeader className="pb-3">
|
|
<CardTitle className="text-base">
|
|
{t('matchesMap.selectedMatch', 'Selected Match')}
|
|
</CardTitle>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<MatchCard
|
|
match={selectedMatchData}
|
|
onViewDetails={handleViewMatchDetails}
|
|
/>
|
|
</CardContent>
|
|
</Card>
|
|
)}
|
|
|
|
{/* Match List */}
|
|
<Card>
|
|
<CardHeader className="pb-3">
|
|
<CardTitle className="text-base">
|
|
{t('matchesMap.matches', 'Matches')}
|
|
<Badge variant="outline" className="ml-2">
|
|
{filteredMatches.length}
|
|
</Badge>
|
|
</CardTitle>
|
|
</CardHeader>
|
|
<CardContent>
|
|
{isLoading ? (
|
|
<div className="space-y-3">
|
|
{[...Array(5)].map((_, i) => (
|
|
<div key={i} className="h-20 bg-muted rounded animate-pulse" />
|
|
))}
|
|
</div>
|
|
) : filteredMatches.length > 0 ? (
|
|
<div className="space-y-3 max-h-96 overflow-y-auto">
|
|
{filteredMatches.slice(0, 20).map((match) => (
|
|
<div
|
|
key={match.ID}
|
|
className={`p-3 border rounded-lg cursor-pointer transition-colors ${
|
|
selectedMatch === match.ID
|
|
? 'border-primary bg-primary/5'
|
|
: 'border-border hover:bg-muted/50'
|
|
}`}
|
|
onClick={() => handleMatchSelect(match.ID)}
|
|
>
|
|
<div className="flex items-center justify-between mb-2">
|
|
<Badge variant={match.Status === 'live' ? 'default' : 'outline'}>
|
|
{t(`matchStatus.${match.Status}`, match.Status)}
|
|
</Badge>
|
|
<span className="text-xs text-muted-foreground">
|
|
{match.DistanceKm.toFixed(1)} km
|
|
</span>
|
|
</div>
|
|
<div className="text-sm">
|
|
<div className="font-medium">
|
|
{Math.round(match.CompatibilityScore * 100)}% compatibility
|
|
</div>
|
|
<div className="text-muted-foreground">
|
|
€{match.EconomicValue.toLocaleString()}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
))}
|
|
{filteredMatches.length > 20 && (
|
|
<div className="text-center pt-2">
|
|
<Button variant="outline" size="sm" onClick={() => navigate('/matching')}>
|
|
{t('matchesMap.viewAll', 'View All Matches')}
|
|
</Button>
|
|
</div>
|
|
)}
|
|
</div>
|
|
) : (
|
|
<div className="text-center py-8 text-muted-foreground">
|
|
<MapPin className="h-12 h-4 mb-4 mx-auto opacity-50 text-current w-12 w-4" />
|
|
<p>{t('matchesMap.noMatches', 'No matches found')}</p>
|
|
<p className="text-xs mt-2">
|
|
{t('matchesMap.adjustFilters', 'Try adjusting your filters')}
|
|
</p>
|
|
</div>
|
|
)}
|
|
</CardContent>
|
|
</Card>
|
|
</Stack>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Map */}
|
|
<main className="flex-1 relative">
|
|
<ModuleErrorBoundary moduleName="matches map">
|
|
<MatchesMap
|
|
matches={filteredMatches}
|
|
selectedMatchId={selectedMatch}
|
|
onMatchSelect={handleMatchSelect}
|
|
/>
|
|
</ModuleErrorBoundary>
|
|
|
|
{/* Sidebar Toggle */}
|
|
<Button
|
|
variant="outline"
|
|
size="sm"
|
|
onClick={() => setIsSidebarOpen(!isSidebarOpen)}
|
|
className="absolute top-4 left-4 z-10 shadow-lg"
|
|
>
|
|
<Filter className="h-4 mr-2 text-current w-4" />
|
|
{isSidebarOpen ? t('matchesMap.hideFilters', 'Hide') : t('matchesMap.showFilters', 'Filters')}
|
|
</Button>
|
|
|
|
{/* Legend */}
|
|
<div className="absolute bottom-4 right-4 bg-background/90 backdrop-blur border rounded-lg p-3 shadow-lg">
|
|
<div className="text-sm font-medium mb-2">{t('matchesMap.legend', 'Legend')}</div>
|
|
<div className="space-y-1 text-xs">
|
|
<div className="flex items-center gap-2">
|
|
<div className="w-4 h-0.5 bg-primary"></div>
|
|
<span>{t('matchesMap.matchConnection', 'Match Connection')}</span>
|
|
</div>
|
|
<div className="flex items-center gap-2">
|
|
<div className="w-3 h-3 bg-primary rounded-full border-2 border-white"></div>
|
|
<span>{t('matchesMap.resourceFlow', 'Resource Flow')}</span>
|
|
</div>
|
|
<div className="flex items-center gap-2">
|
|
<div className="w-2 h-2 bg-success rounded-full"></div>
|
|
<span>{t('matchesMap.liveMatch', 'Live Match')}</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</main>
|
|
</div>
|
|
</div>
|
|
</Container>
|
|
</MainLayout>
|
|
);
|
|
};
|
|
|
|
const MatchesMapView = () => (
|
|
<MapProvider>
|
|
<MatchesMapContent />
|
|
</MapProvider>
|
|
);
|
|
|
|
export default MatchesMapView;
|