Skip to content

Commit a1587b6

Browse files
committed
nigga
1 parent 77d90f4 commit a1587b6

6 files changed

Lines changed: 201 additions & 15 deletions

File tree

src/App.tsx

Lines changed: 74 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,11 @@
11
import { useState, useCallback, useMemo, useEffect } from "react"
22
import { MapContainer } from "./components/Map/MapContainer"
33
import { useMapData } from "./hooks/useMapData"
4+
import {
5+
FreeSpotsFilter,
6+
ZoneSelector,
7+
type FreeSpotFilterValue,
8+
} from "./components/Filters"
49
import type { MapState } from "./types"
510
import type { Zone } from "./types/api"
611
import "./App.css"
@@ -11,33 +16,54 @@ function App() {
1116
zoom: 12,
1217
})
1318

19+
const [freeSpotFilter, setFreeSpotFilter] =
20+
useState<FreeSpotFilterValue>("all")
21+
const [selectedZoneId, setSelectedZoneId] = useState<number | null>(null)
22+
1423
const { zones, loading, error, total, refetch } = useMapData({
1524
autoFetch: true,
1625
})
1726

27+
const filteredZones = useMemo(() => {
28+
return zones.filter((zone) => {
29+
const freeSpots =
30+
zone.occupied !== undefined ? zone.capacity - zone.occupied : 0
31+
32+
switch (freeSpotFilter) {
33+
case "one":
34+
return freeSpots === 1
35+
case "twoOrMore":
36+
return freeSpots >= 2
37+
case "all":
38+
default:
39+
return true
40+
}
41+
})
42+
}, [zones, freeSpotFilter])
43+
1844
const totalFreeSpots = useMemo(
1945
() =>
20-
zones.reduce((acc, zone) => {
46+
filteredZones.reduce((acc, zone) => {
2147
const occupied = zone.occupied
2248
const capacity = zone.capacity
2349
if (occupied !== undefined) {
2450
return acc + (capacity - occupied)
2551
}
2652
return acc
2753
}, 0),
28-
[zones]
54+
[filteredZones]
2955
)
3056

3157
const totalCapacity = useMemo(
3258
() =>
33-
zones.reduce((acc, zone) => {
59+
filteredZones.reduce((acc, zone) => {
3460
const capacity = zone.capacity
3561
return acc + capacity
3662
}, 0),
37-
[zones]
63+
[filteredZones]
3864
)
3965

40-
const handleZoneClick = useCallback((zone: Zone) => {
66+
const focusOnZone = useCallback((zone: Zone) => {
4167
const points = zone.points
4268
if (points && points.length > 0) {
4369
const centerLat =
@@ -46,12 +72,31 @@ function App() {
4672
points.reduce((sum, p) => sum + p.longitude, 0) / points.length
4773

4874
setMapState((prev) => ({
49-
...prev,
5075
center: [centerLat, centerLng],
76+
zoom: Math.max(prev.zoom, 18),
5177
}))
5278
}
5379
}, [])
5480

81+
const handleZoneClick = useCallback(
82+
(zone: Zone) => {
83+
focusOnZone(zone)
84+
},
85+
[focusOnZone]
86+
)
87+
88+
const handleZoneSelect = useCallback(
89+
(zone: Zone | null) => {
90+
if (zone) {
91+
setSelectedZoneId(zone.zone_id)
92+
focusOnZone(zone)
93+
} else {
94+
setSelectedZoneId(null)
95+
}
96+
},
97+
[focusOnZone]
98+
)
99+
55100
const handleMapStateChange = useCallback((newState: MapState) => {
56101
setMapState(newState)
57102
}, [])
@@ -70,19 +115,40 @@ function App() {
70115
<main className="mx-auto">
71116
<div className="relative h-[calc(100vh)] w-full">
72117
<MapContainer
73-
zones={zones}
118+
zones={filteredZones}
74119
mapState={mapState}
75120
onMapStateChange={handleMapStateChange}
76121
onZoneClick={handleZoneClick}
77122
className="w-full h-full"
78123
/>
79124

125+
<div className="absolute top-2 left-16 right-2 sm:top-4 sm:left-16 sm:right-auto sm:max-w-md z-[1000] bg-white/90 backdrop-blur-sm rounded-lg shadow-lg border border-gray-200">
126+
<div className="p-3 sm:p-4">
127+
<div className="flex flex-col gap-3">
128+
<div>
129+
<h3 className="text-xs sm:text-sm font-semibold text-gray-700 mb-2">
130+
Фильтр по свободным местам
131+
</h3>
132+
<FreeSpotsFilter
133+
value={freeSpotFilter}
134+
onChange={setFreeSpotFilter}
135+
/>
136+
</div>
137+
<ZoneSelector
138+
zones={zones}
139+
selectedZoneId={selectedZoneId}
140+
onZoneSelect={handleZoneSelect}
141+
/>
142+
</div>
143+
</div>
144+
</div>
145+
80146
<div className="absolute bottom-2 left-2 right-2 sm:bottom-4 sm:left-4 sm:right-4 z-[1000] bg-white/70 backdrop-blur-sm rounded-lg shadow-lg border border-gray-200 map-overlay">
81147
<div className="flex items-center justify-between p-1 sm:p-2 min-w-0">
82148
<div className="flex items-center space-x-2 sm:space-x-3 min-w-0 flex-1 flex-wrap gap-1">
83149
{total > 0 && (
84150
<span className="text-xs sm:text-sm text-gray-700">
85-
Зон: {total}
151+
Зон: {filteredZones.length}/{total}
86152
</span>
87153
)}
88154
{totalCapacity > 0 && (
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import React from "react"
2+
3+
export type FreeSpotFilterValue = "all" | "one" | "twoOrMore"
4+
5+
interface FreeSpotsFilterProps {
6+
value: FreeSpotFilterValue
7+
onChange: (value: FreeSpotFilterValue) => void
8+
}
9+
10+
export const FreeSpotsFilter: React.FC<FreeSpotsFilterProps> = ({
11+
value,
12+
onChange,
13+
}) => {
14+
const filters: { value: FreeSpotFilterValue; label: string }[] = [
15+
{ value: "all", label: "Все" },
16+
{ value: "one", label: "1 свободное место" },
17+
{ value: "twoOrMore", label: "2 и более свободных мест" },
18+
]
19+
20+
return (
21+
<div className="flex flex-wrap gap-2">
22+
{filters.map((filter) => (
23+
<button
24+
key={filter.value}
25+
onClick={() => onChange(filter.value)}
26+
className={`px-3 py-1.5 text-xs sm:text-sm font-medium rounded-lg transition-colors ${
27+
value === filter.value
28+
? "bg-blue-600 text-white"
29+
: "bg-white text-gray-700 hover:bg-gray-100 border border-gray-300"
30+
}`}
31+
>
32+
{filter.label}
33+
</button>
34+
))}
35+
</div>
36+
)
37+
}
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
import React from "react"
2+
import type { Zone } from "../../types/api"
3+
4+
interface ZoneSelectorProps {
5+
zones: Zone[]
6+
selectedZoneId: number | null
7+
onZoneSelect: (zone: Zone | null) => void
8+
}
9+
10+
export const ZoneSelector: React.FC<ZoneSelectorProps> = ({
11+
zones,
12+
selectedZoneId,
13+
onZoneSelect,
14+
}) => {
15+
const handleChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
16+
const value = e.target.value
17+
if (value === "") {
18+
onZoneSelect(null)
19+
} else {
20+
const zone = zones.find((z) => z.zone_id === Number(value))
21+
if (zone) {
22+
onZoneSelect(zone)
23+
}
24+
}
25+
}
26+
27+
return (
28+
<div className="flex items-center gap-2">
29+
<label
30+
htmlFor="zone-selector"
31+
className="text-xs sm:text-sm font-medium text-gray-700 whitespace-nowrap"
32+
>
33+
Перейти к зоне:
34+
</label>
35+
<select
36+
id="zone-selector"
37+
value={selectedZoneId ?? ""}
38+
onChange={handleChange}
39+
className="px-2 py-1.5 text-xs sm:text-sm border border-gray-300 rounded-lg bg-white text-gray-900 focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
40+
>
41+
<option value="">Выберите зону</option>
42+
{zones.map((zone) => (
43+
<option key={zone.zone_id} value={zone.zone_id}>
44+
Зона {zone.zone_id}
45+
</option>
46+
))}
47+
</select>
48+
</div>
49+
)
50+
}

src/components/Filters/index.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
export { FreeSpotsFilter } from "./FreeSpotsFilter"
2+
export type { FreeSpotFilterValue } from "./FreeSpotsFilter"
3+
export { ZoneSelector } from "./ZoneSelector"

src/components/Map/MapContainer.tsx

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,27 @@ const MapEventHandler: React.FC<{
4747
return null
4848
}
4949

50+
const MapViewController: React.FC<{ mapState: MapState }> = ({ mapState }) => {
51+
const map = useMap()
52+
53+
useEffect(() => {
54+
const currentCenter = map.getCenter()
55+
const currentZoom = map.getZoom()
56+
57+
const [newLat, newLng] = mapState.center
58+
const centerChanged =
59+
Math.abs(currentCenter.lat - newLat) > 0.000001 ||
60+
Math.abs(currentCenter.lng - newLng) > 0.000001
61+
const zoomChanged = currentZoom !== mapState.zoom
62+
63+
if (centerChanged || zoomChanged) {
64+
map.setView(mapState.center, mapState.zoom)
65+
}
66+
}, [map, mapState])
67+
68+
return null
69+
}
70+
5071
export const MapContainer: React.FC<MapContainerProps> = ({
5172
zones,
5273
mapState,
@@ -70,6 +91,7 @@ export const MapContainer: React.FC<MapContainerProps> = ({
7091
url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
7192
/>
7293

94+
<MapViewController mapState={mapState} />
7395
<MapEventHandler onMapStateChange={onMapStateChange} />
7496

7597
<MapPoints zones={zones} onZoneClick={onZoneClick} />

src/components/Map/MapPoints.tsx

Lines changed: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -37,9 +37,9 @@ const createZoneIcon = (freeSpots: number | undefined) => {
3737
const calculateCenterLine = (points: Point[]): [number, number][] => {
3838
if (!points || points.length !== 4) return []
3939

40-
const [p0, p1, p2] = points
40+
const [p0, p1, p2, p3] = points
4141

42-
if (!p0 || !p1 || !p2) return []
42+
if (!p0 || !p1 || !p2 || !p3) return []
4343

4444
const dist1 = Math.sqrt(
4545
Math.pow(p1.latitude - p0.latitude, 2) +
@@ -50,15 +50,23 @@ const calculateCenterLine = (points: Point[]): [number, number][] => {
5050
Math.pow(p2.longitude - p1.longitude, 2)
5151
)
5252

53-
if (dist1 > dist2) {
53+
if (dist1 < dist2) {
54+
const midShort1Lat = (p0.latitude + p1.latitude) / 2
55+
const midShort1Lng = (p0.longitude + p1.longitude) / 2
56+
const midShort2Lat = (p2.latitude + p3.latitude) / 2
57+
const midShort2Lng = (p2.longitude + p3.longitude) / 2
5458
return [
55-
[p0.latitude, p0.longitude],
56-
[p1.latitude, p1.longitude],
59+
[midShort1Lat, midShort1Lng],
60+
[midShort2Lat, midShort2Lng],
5761
]
5862
} else {
63+
const midShort1Lat = (p1.latitude + p2.latitude) / 2
64+
const midShort1Lng = (p1.longitude + p2.longitude) / 2
65+
const midShort2Lat = (p3.latitude + p0.latitude) / 2
66+
const midShort2Lng = (p3.longitude + p0.longitude) / 2
5967
return [
60-
[p1.latitude, p1.longitude],
61-
[p2.latitude, p2.longitude],
68+
[midShort1Lat, midShort1Lng],
69+
[midShort2Lat, midShort2Lng],
6270
]
6371
}
6472
}

0 commit comments

Comments
 (0)