Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 46 additions & 0 deletions .air.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
root = ""
testdata_dir = "testdata"
tmp_dir = "tmp"

[build]
args_bin = []
bin = "./main frontend"
cmd = "go build ./cmd/hotrod/main.go"
delay = 1000
exclude_dir = ["assets", "tmp", "vendor", "testdata", "services/frontend/web_assets/"]
exclude_file = []
exclude_regex = ["_test.go"]
exclude_unchanged = false
follow_symlink = false
full_bin = ""
include_dir = ["services/frontend"]
include_ext = ["go", "tpl", "tmpl", "html", "tsx", "ts"]
include_file = []
kill_delay = "0s"
log = "build-errors.log"
poll = false
poll_interval = 0
post_cmd = []
pre_cmd = ["make build-frontend-app"]
rerun = true
rerun_delay = 500
send_interrupt = false
stop_on_error = true

[color]
app = ""
build = "yellow"
main = "magenta"
runner = "green"
watcher = "cyan"

[log]
main_only = false
time = false

[misc]
clean_on_exit = false

[screen]
clear_on_rebuild = true
keep_scroll = true
27 changes: 27 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,3 +24,30 @@ To uninstall:
```bash
kubectl delete ns "${NAMESPACE}"
```


## Development

### Frontend

To run frontend you could easily run with `air` that helps with hot-reload.

Before running `air` or manual steps you have to set up the following env
```shell
export KAFKA_BROKER=kafka-headless.${NAMESPACE}.svc:9092
export REDIS_ADDR=redis.${NAMESPACE}.svc:6379
export FRONTEND_LOCATION_ADDR=location.${NAMESPACE}.svc:8081
```

Now let's run the frontend
```shell
air
```

That will listen for the changes and restart the server every change.

If no want to use this approach, you could
```shell
make build-frontend-app
go run ./cmd/hotrod/main.go frontend
```
6 changes: 5 additions & 1 deletion services/frontend/react_app/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,16 +13,20 @@
"@chakra-ui/react": "^2.8.2",
"@emotion/react": "^11.11.4",
"@emotion/styled": "^11.11.0",
"@faker-js/faker": "^9.3.0",
"framer-motion": "^11.0.8",
"geojson-path-finder": "^2.0.2",
"leaflet": "^1.9.4",
"leaflet-routing-machine": "^3.2.12",
"pigeon-maps": "^0.21.4",
"react": "^18.2.0",
"react-countdown": "^2.3.6",
"react-dom": "^18.2.0",
"react-leaflet": "^4.2.1"
},
"devDependencies": {
"@types/leaflet": "^1.9.8",
"@types/leaflet": "^1.9.15",
"@types/leaflet-routing-machine": "^3.2.8",
"@types/react": "^18.2.56",
"@types/react-dom": "^18.2.19",
"@typescript-eslint/eslint-plugin": "^7.0.2",
Expand Down
34 changes: 29 additions & 5 deletions services/frontend/react_app/src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,37 @@
import './App.css'
import {HomePage} from "./pages/home.tsx";
import {SessionProvider} from "./context/sessionContext/context.tsx";
import {ChakraProvider, extendTheme} from "@chakra-ui/react";


import { accordionAnatomy } from '@chakra-ui/anatomy'
import { createMultiStyleConfigHelpers } from '@chakra-ui/react'

const { definePartsStyle, defineMultiStyleConfig } =
createMultiStyleConfigHelpers(accordionAnatomy.keys)

const baseStyle = definePartsStyle({
container: {
borderColor: 'gray.400',
},
})


const accordionTheme = defineMultiStyleConfig({ baseStyle })

function App() {
return (
<SessionProvider>
<HomePage />
</SessionProvider>
)

const theme = extendTheme({
components: { Accordion: accordionTheme },
})

return (
<ChakraProvider theme={theme}>
<SessionProvider>
<HomePage/>
</SessionProvider>
</ChakraProvider>
)
}

export default App
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ export const LocationSelect = ({ placeholder, locations, selectedLocationID, onS
>
{ locations.map(loc => {
return (
<option value={loc.ID}>{loc.Name}</option>
<option value={loc.id}>{loc.name}</option>
)
})}
</Select>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ const BaseLog = ({ log }: BaseLogProps) => {
<h2>
<AccordionButton>
<Box as="span" flex='1' textAlign='left'>
Request ID: #{requestID} from <LocationHighlight value={pickupLocation.Name} type='pickup'/> to <LocationHighlight value={dropoffLocation.Name} type='dropoff'/>
Request ID: #{requestID} from <LocationHighlight value={pickupLocation.name} type='pickup'/> to <LocationHighlight value={dropoffLocation.name} type='dropoff'/>
</Box>
<AccordionIcon />
</AccordionButton>
Expand Down
107 changes: 101 additions & 6 deletions services/frontend/react_app/src/components/features/map/map.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,108 @@
import React, {useEffect, useMemo} from "react";
import {MapContainer, TileLayer, useMap} from "react-leaflet";
import "leaflet/dist/leaflet.css";
import * as L from 'leaflet';
import {LatLngTuple} from 'leaflet';
import "leaflet-routing-machine";
import {Flex} from "@chakra-ui/react";
import {Marker, Map as PigeonMap} from "pigeon-maps";

interface RouteMapProps {
start?: [number, number];
end?: [number, number];
center: LatLngTuple;
}

const COORDINATES_MOCKED: Record<number, [number, number]> = {
1: [37.7749, -122.4194], // My Home (San Francisco, downtown)
123: [37.7764, -122.4241], // Rachel's Floral Designs (near Hayes Valley)
392: [37.7723, -122.4108], // Trom Chocolatier (Mission District)
567: [37.7807, -122.4081], // Amazing Coffee Roasters (SoMa)
731: [37.7689, -122.4494] // Japanese Desserts (near Golden Gate Park)
};

const getCenter = (start: [number, number] | undefined, end: [number, number] | undefined): LatLngTuple => {
if (!start || !end) {
return [37.562304, -122.32668]
}

return [(start[0] + end[0]) / 2, (start[1] + end[1]) / 2];
}

const RoutingControl = ({ start, end, center }: RouteMapProps) => {
const map = useMap();

useEffect(() => {
if (!map || !start || !end) return () => {};

map.setView(center);

const customPlan = L.Routing.plan([L.latLng(start), L.latLng(end)], {
createMarker: (i, waypoint) => {
return L.marker(waypoint.latLng, {
icon: L.icon({
iconUrl: i === 0
? "https://cdn-icons-png.flaticon.com/512/2991/2991122.png" // Start marker
: "https://cdn-icons-png.flaticon.com/512/190/190411.png", // End marker
iconSize: [25, 41],
iconAnchor: [12, 41],
}),
});
}, // Prevent default markers
});

const routingControl = L.Routing.control({
waypoints: [L.latLng(start[0], start[1]), L.latLng(end[0], end[1])],
routeWhileDragging: true,
addWaypoints: false,
lineOptions: {
styles: [{ color: "blue", weight: 6 }], // Wider path
extendToWaypoints: true, // Default is true, ensures lines extend to waypoints
missingRouteTolerance: 10, // Default is 10 (meters)
},
plan:customPlan,
}).addTo(map);

return () => {
map.removeControl(routingControl);
};
}, [map, start, end, center]);

return null;
};

const RouteMap: React.FC<Omit<RouteMapProps, "center">> = ({start, end}) => {
const center = useMemo((): LatLngTuple => {
return getCenter(start, end);
}, [start, end]);

return (
<MapContainer
center={center}
zoom={17}
style={{height: "100%", width: "100%"}}
>
<TileLayer
url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
attribution='&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
/>
<RoutingControl start={start} end={end} center={center}/>
</MapContainer>
);
};


type MapProps = {
dropoffLocationID: number;
pickupLocationID: number;
}

export const Map = ({dropoffLocationID, pickupLocationID}: MapProps) => {
const dropoffCoords = COORDINATES_MOCKED[dropoffLocationID as keyof typeof COORDINATES_MOCKED];
const pickupCoords = COORDINATES_MOCKED[pickupLocationID as keyof typeof COORDINATES_MOCKED];

export const Map = () => {
return (
<Flex w='100%' h='100%' borderRadius={16} overflow='hidden'>
<PigeonMap defaultCenter={[37.562304, -122.32668]} defaultZoom={17}>
<Marker width={50} anchor={[37.562304, -122.32668]} />
</PigeonMap>
<RouteMap start={pickupCoords} end={dropoffCoords} />
</Flex>
)
);
}
59 changes: 59 additions & 0 deletions services/frontend/react_app/src/hooks/useGetRequestArrival.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import {Log, LogEntry} from "./useLogs.tsx";
import {useEffect, useState} from "react";
import { faker } from "@faker-js/faker";

export const useGetRequestArrival = (logs: Log[]) => {
const [driverArrival, setDriverArrival] = useState<number | undefined>();
const [driverDetails, setDriverDetails] = useState({
name: "",
plate: "",
});

const parseDriverLogService = (entry: LogEntry) => {
if (entry.service !== 'driver') return;

const timeRegex = /(\d+)m(\d+)s/;
const match = entry.status.match(timeRegex);

if (!match) return;

const minutes = parseInt(match[1], 10);
const seconds = parseInt(match[2], 10);

return minutes * 60 + seconds;
};

useEffect(() => {
if (logs.length === 0) return;

const lastRequestDrive = logs[0];

setDriverArrival(undefined);

if (!lastRequestDrive) {
setDriverArrival(undefined);
return;
}

const driverEntries = lastRequestDrive.entries.filter((e) => e.service === 'driver');
const parsedTime = driverEntries
.map((e) => parseDriverLogService(e))
.find((e) => e !== undefined);

const plate = driverEntries.map(e => {
return e.status.match(/Driver\s+(.*?)\s+arriving/)
}).find(e => e !== null);

setDriverDetails({
name: faker.person.fullName(),
plate: plate && plate.length > 1 ? plate[1] : "Unknown",
});
setDriverArrival(parsedTime);
}, [logs]);


return {
driverArrival,
driverDetails,
};
};
1 change: 0 additions & 1 deletion services/frontend/react_app/src/hooks/useLogs.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@ export type LogEntry = {
export const useLogs = () => {
const [logs, setLogs] = useState<Log[]>([]);


const addNewLog = (pickupLocation: Location, dropoffLocation: Location, requestID: number, log: LogEntry) => {
setLogs(prev => [{ pickupLocation, dropoffLocation, requestID, entries: [log]}, ...prev])
}
Expand Down
44 changes: 44 additions & 0 deletions services/frontend/react_app/src/pages/home.module.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
.drawer {
position: fixed;
top: 0;
right: 0;
width: 700px;
height: 100%;
background: white;
box-shadow: -2px 0 5px rgba(0, 0, 0, 0.1);
transform: translateX(100%);
transition: transform 0.3s ease;
z-index: 999999;
display: flex;
flex-direction: column; /* Ensures vertical stacking */

gap: 1rem;
}

.drawer.open {
transform: translateX(0);
}

.drawer-header {
padding: 1rem;
border-bottom: 1px solid #ddd;
text-align: left;
}

.drawer-body {
flex: 1; /* Fills available space */
padding: 1rem;
overflow-y: auto; /* Allows scrolling when content overflows */
}

.drawer-footer {
padding: 1rem;
border-top: 1px solid #ddd;
background: white;
}



.leaflet-control { z-index: 0 !important}
.leaflet-pane { z-index: 0 !important}
.leaflet-top, .leaflet-bottom {z-index: 0 !important}
Loading
Loading