Skip to content

Commit edd5d49

Browse files
davixckyfoxish
andauthored
make ui more app real (#253)
* add air config for handling frontend ui changes * move logs to sidebar and make map live * add instructions for how to use air * add driver arrival and driver name * Update README.md * Update home.tsx * add driver plate * fix spacing and coloring cosmetic --------- Co-authored-by: Anirudh Ramanathan <anirudh@foxish.me>
1 parent 2e9b7e4 commit edd5d49

File tree

18 files changed

+935
-1261
lines changed

18 files changed

+935
-1261
lines changed

.air.toml

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
root = ""
2+
testdata_dir = "testdata"
3+
tmp_dir = "tmp"
4+
5+
[build]
6+
args_bin = []
7+
bin = "./main frontend"
8+
cmd = "go build ./cmd/hotrod/main.go"
9+
delay = 1000
10+
exclude_dir = ["assets", "tmp", "vendor", "testdata", "services/frontend/web_assets/"]
11+
exclude_file = []
12+
exclude_regex = ["_test.go"]
13+
exclude_unchanged = false
14+
follow_symlink = false
15+
full_bin = ""
16+
include_dir = ["services/frontend"]
17+
include_ext = ["go", "tpl", "tmpl", "html", "tsx", "ts"]
18+
include_file = []
19+
kill_delay = "0s"
20+
log = "build-errors.log"
21+
poll = false
22+
poll_interval = 0
23+
post_cmd = []
24+
pre_cmd = ["make build-frontend-app"]
25+
rerun = true
26+
rerun_delay = 500
27+
send_interrupt = false
28+
stop_on_error = true
29+
30+
[color]
31+
app = ""
32+
build = "yellow"
33+
main = "magenta"
34+
runner = "green"
35+
watcher = "cyan"
36+
37+
[log]
38+
main_only = false
39+
time = false
40+
41+
[misc]
42+
clean_on_exit = false
43+
44+
[screen]
45+
clear_on_rebuild = true
46+
keep_scroll = true

README.md

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,3 +24,30 @@ To uninstall:
2424
```bash
2525
kubectl delete ns "${NAMESPACE}"
2626
```
27+
28+
29+
## Development
30+
31+
### Frontend
32+
33+
To run frontend you could easily run with `air` that helps with hot-reload.
34+
35+
Before running `air` or manual steps you have to set up the following env
36+
```shell
37+
export KAFKA_BROKER=kafka-headless.${NAMESPACE}.svc:9092
38+
export REDIS_ADDR=redis.${NAMESPACE}.svc:6379
39+
export FRONTEND_LOCATION_ADDR=location.${NAMESPACE}.svc:8081
40+
```
41+
42+
Now let's run the frontend
43+
```shell
44+
air
45+
```
46+
47+
That will listen for the changes and restart the server every change.
48+
49+
If no want to use this approach, you could
50+
```shell
51+
make build-frontend-app
52+
go run ./cmd/hotrod/main.go frontend
53+
```

services/frontend/react_app/package.json

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,16 +13,20 @@
1313
"@chakra-ui/react": "^2.8.2",
1414
"@emotion/react": "^11.11.4",
1515
"@emotion/styled": "^11.11.0",
16+
"@faker-js/faker": "^9.3.0",
1617
"framer-motion": "^11.0.8",
1718
"geojson-path-finder": "^2.0.2",
1819
"leaflet": "^1.9.4",
20+
"leaflet-routing-machine": "^3.2.12",
1921
"pigeon-maps": "^0.21.4",
2022
"react": "^18.2.0",
23+
"react-countdown": "^2.3.6",
2124
"react-dom": "^18.2.0",
2225
"react-leaflet": "^4.2.1"
2326
},
2427
"devDependencies": {
25-
"@types/leaflet": "^1.9.8",
28+
"@types/leaflet": "^1.9.15",
29+
"@types/leaflet-routing-machine": "^3.2.8",
2630
"@types/react": "^18.2.56",
2731
"@types/react-dom": "^18.2.19",
2832
"@typescript-eslint/eslint-plugin": "^7.0.2",
Lines changed: 29 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,37 @@
11
import './App.css'
22
import {HomePage} from "./pages/home.tsx";
33
import {SessionProvider} from "./context/sessionContext/context.tsx";
4+
import {ChakraProvider, extendTheme} from "@chakra-ui/react";
5+
6+
7+
import { accordionAnatomy } from '@chakra-ui/anatomy'
8+
import { createMultiStyleConfigHelpers } from '@chakra-ui/react'
9+
10+
const { definePartsStyle, defineMultiStyleConfig } =
11+
createMultiStyleConfigHelpers(accordionAnatomy.keys)
12+
13+
const baseStyle = definePartsStyle({
14+
container: {
15+
borderColor: 'gray.400',
16+
},
17+
})
18+
19+
20+
const accordionTheme = defineMultiStyleConfig({ baseStyle })
421

522
function App() {
6-
return (
7-
<SessionProvider>
8-
<HomePage />
9-
</SessionProvider>
10-
)
23+
24+
const theme = extendTheme({
25+
components: { Accordion: accordionTheme },
26+
})
27+
28+
return (
29+
<ChakraProvider theme={theme}>
30+
<SessionProvider>
31+
<HomePage/>
32+
</SessionProvider>
33+
</ChakraProvider>
34+
)
1135
}
1236

1337
export default App

services/frontend/react_app/src/components/common/locationSelect/locationSelect.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ export const LocationSelect = ({ placeholder, locations, selectedLocationID, onS
2424
>
2525
{ locations.map(loc => {
2626
return (
27-
<option value={loc.ID}>{loc.Name}</option>
27+
<option value={loc.id}>{loc.name}</option>
2828
)
2929
})}
3030
</Select>

services/frontend/react_app/src/components/features/logs/logs.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,7 @@ const BaseLog = ({ log }: BaseLogProps) => {
7474
<h2>
7575
<AccordionButton>
7676
<Box as="span" flex='1' textAlign='left'>
77-
Request ID: #{requestID} from <LocationHighlight value={pickupLocation.Name} type='pickup'/> to <LocationHighlight value={dropoffLocation.Name} type='dropoff'/>
77+
Request ID: #{requestID} from <LocationHighlight value={pickupLocation.name} type='pickup'/> to <LocationHighlight value={dropoffLocation.name} type='dropoff'/>
7878
</Box>
7979
<AccordionIcon />
8080
</AccordionButton>
Lines changed: 101 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,108 @@
1+
import React, {useEffect, useMemo} from "react";
2+
import {MapContainer, TileLayer, useMap} from "react-leaflet";
3+
import "leaflet/dist/leaflet.css";
4+
import * as L from 'leaflet';
5+
import {LatLngTuple} from 'leaflet';
6+
import "leaflet-routing-machine";
17
import {Flex} from "@chakra-ui/react";
2-
import {Marker, Map as PigeonMap} from "pigeon-maps";
38

9+
interface RouteMapProps {
10+
start?: [number, number];
11+
end?: [number, number];
12+
center: LatLngTuple;
13+
}
14+
15+
const COORDINATES_MOCKED: Record<number, [number, number]> = {
16+
1: [37.7749, -122.4194], // My Home (San Francisco, downtown)
17+
123: [37.7764, -122.4241], // Rachel's Floral Designs (near Hayes Valley)
18+
392: [37.7723, -122.4108], // Trom Chocolatier (Mission District)
19+
567: [37.7807, -122.4081], // Amazing Coffee Roasters (SoMa)
20+
731: [37.7689, -122.4494] // Japanese Desserts (near Golden Gate Park)
21+
};
22+
23+
const getCenter = (start: [number, number] | undefined, end: [number, number] | undefined): LatLngTuple => {
24+
if (!start || !end) {
25+
return [37.562304, -122.32668]
26+
}
27+
28+
return [(start[0] + end[0]) / 2, (start[1] + end[1]) / 2];
29+
}
30+
31+
const RoutingControl = ({ start, end, center }: RouteMapProps) => {
32+
const map = useMap();
33+
34+
useEffect(() => {
35+
if (!map || !start || !end) return () => {};
36+
37+
map.setView(center);
38+
39+
const customPlan = L.Routing.plan([L.latLng(start), L.latLng(end)], {
40+
createMarker: (i, waypoint) => {
41+
return L.marker(waypoint.latLng, {
42+
icon: L.icon({
43+
iconUrl: i === 0
44+
? "https://cdn-icons-png.flaticon.com/512/2991/2991122.png" // Start marker
45+
: "https://cdn-icons-png.flaticon.com/512/190/190411.png", // End marker
46+
iconSize: [25, 41],
47+
iconAnchor: [12, 41],
48+
}),
49+
});
50+
}, // Prevent default markers
51+
});
52+
53+
const routingControl = L.Routing.control({
54+
waypoints: [L.latLng(start[0], start[1]), L.latLng(end[0], end[1])],
55+
routeWhileDragging: true,
56+
addWaypoints: false,
57+
lineOptions: {
58+
styles: [{ color: "blue", weight: 6 }], // Wider path
59+
extendToWaypoints: true, // Default is true, ensures lines extend to waypoints
60+
missingRouteTolerance: 10, // Default is 10 (meters)
61+
},
62+
plan:customPlan,
63+
}).addTo(map);
64+
65+
return () => {
66+
map.removeControl(routingControl);
67+
};
68+
}, [map, start, end, center]);
69+
70+
return null;
71+
};
72+
73+
const RouteMap: React.FC<Omit<RouteMapProps, "center">> = ({start, end}) => {
74+
const center = useMemo((): LatLngTuple => {
75+
return getCenter(start, end);
76+
}, [start, end]);
77+
78+
return (
79+
<MapContainer
80+
center={center}
81+
zoom={17}
82+
style={{height: "100%", width: "100%"}}
83+
>
84+
<TileLayer
85+
url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
86+
attribution='&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
87+
/>
88+
<RoutingControl start={start} end={end} center={center}/>
89+
</MapContainer>
90+
);
91+
};
92+
93+
94+
type MapProps = {
95+
dropoffLocationID: number;
96+
pickupLocationID: number;
97+
}
98+
99+
export const Map = ({dropoffLocationID, pickupLocationID}: MapProps) => {
100+
const dropoffCoords = COORDINATES_MOCKED[dropoffLocationID as keyof typeof COORDINATES_MOCKED];
101+
const pickupCoords = COORDINATES_MOCKED[pickupLocationID as keyof typeof COORDINATES_MOCKED];
4102

5-
export const Map = () => {
6103
return (
7104
<Flex w='100%' h='100%' borderRadius={16} overflow='hidden'>
8-
<PigeonMap defaultCenter={[37.562304, -122.32668]} defaultZoom={17}>
9-
<Marker width={50} anchor={[37.562304, -122.32668]} />
10-
</PigeonMap>
105+
<RouteMap start={pickupCoords} end={dropoffCoords} />
11106
</Flex>
12-
)
107+
);
13108
}
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
import {Log, LogEntry} from "./useLogs.tsx";
2+
import {useEffect, useState} from "react";
3+
import { faker } from "@faker-js/faker";
4+
5+
export const useGetRequestArrival = (logs: Log[]) => {
6+
const [driverArrival, setDriverArrival] = useState<number | undefined>();
7+
const [driverDetails, setDriverDetails] = useState({
8+
name: "",
9+
plate: "",
10+
});
11+
12+
const parseDriverLogService = (entry: LogEntry) => {
13+
if (entry.service !== 'driver') return;
14+
15+
const timeRegex = /(\d+)m(\d+)s/;
16+
const match = entry.status.match(timeRegex);
17+
18+
if (!match) return;
19+
20+
const minutes = parseInt(match[1], 10);
21+
const seconds = parseInt(match[2], 10);
22+
23+
return minutes * 60 + seconds;
24+
};
25+
26+
useEffect(() => {
27+
if (logs.length === 0) return;
28+
29+
const lastRequestDrive = logs[0];
30+
31+
setDriverArrival(undefined);
32+
33+
if (!lastRequestDrive) {
34+
setDriverArrival(undefined);
35+
return;
36+
}
37+
38+
const driverEntries = lastRequestDrive.entries.filter((e) => e.service === 'driver');
39+
const parsedTime = driverEntries
40+
.map((e) => parseDriverLogService(e))
41+
.find((e) => e !== undefined);
42+
43+
const plate = driverEntries.map(e => {
44+
return e.status.match(/Driver\s+(.*?)\s+arriving/)
45+
}).find(e => e !== null);
46+
47+
setDriverDetails({
48+
name: faker.person.fullName(),
49+
plate: plate && plate.length > 1 ? plate[1] : "Unknown",
50+
});
51+
setDriverArrival(parsedTime);
52+
}, [logs]);
53+
54+
55+
return {
56+
driverArrival,
57+
driverDetails,
58+
};
59+
};

services/frontend/react_app/src/hooks/useLogs.tsx

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,6 @@ export type LogEntry = {
2020
export const useLogs = () => {
2121
const [logs, setLogs] = useState<Log[]>([]);
2222

23-
2423
const addNewLog = (pickupLocation: Location, dropoffLocation: Location, requestID: number, log: LogEntry) => {
2524
setLogs(prev => [{ pickupLocation, dropoffLocation, requestID, entries: [log]}, ...prev])
2625
}
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
.drawer {
2+
position: fixed;
3+
top: 0;
4+
right: 0;
5+
width: 700px;
6+
height: 100%;
7+
background: white;
8+
box-shadow: -2px 0 5px rgba(0, 0, 0, 0.1);
9+
transform: translateX(100%);
10+
transition: transform 0.3s ease;
11+
z-index: 999999;
12+
display: flex;
13+
flex-direction: column; /* Ensures vertical stacking */
14+
15+
gap: 1rem;
16+
}
17+
18+
.drawer.open {
19+
transform: translateX(0);
20+
}
21+
22+
.drawer-header {
23+
padding: 1rem;
24+
border-bottom: 1px solid #ddd;
25+
text-align: left;
26+
}
27+
28+
.drawer-body {
29+
flex: 1; /* Fills available space */
30+
padding: 1rem;
31+
overflow-y: auto; /* Allows scrolling when content overflows */
32+
}
33+
34+
.drawer-footer {
35+
padding: 1rem;
36+
border-top: 1px solid #ddd;
37+
background: white;
38+
}
39+
40+
41+
42+
.leaflet-control { z-index: 0 !important}
43+
.leaflet-pane { z-index: 0 !important}
44+
.leaflet-top, .leaflet-bottom {z-index: 0 !important}

0 commit comments

Comments
 (0)