Skip to content

Commit

Permalink
feat: add location selector
Browse files Browse the repository at this point in the history
  • Loading branch information
boojack committed Sep 25, 2024
1 parent 63989ab commit b144faf
Show file tree
Hide file tree
Showing 12 changed files with 651 additions and 375 deletions.
2 changes: 2 additions & 0 deletions proto/api/v1/memo_service.proto
Original file line number Diff line number Diff line change
Expand Up @@ -215,6 +215,8 @@ message CreateMemoRequest {
repeated Resource resources = 3;

repeated MemoRelation relations = 4;

optional Location location = 5;
}

message ListMemosRequest {
Expand Down
758 changes: 386 additions & 372 deletions proto/gen/api/v1/memo_service.pb.go

Large diffs are not rendered by default.

2 changes: 2 additions & 0 deletions proto/gen/apidocs.swagger.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2384,6 +2384,8 @@ definitions:
items:
type: object
$ref: '#/definitions/v1MemoRelation'
location:
$ref: '#/definitions/apiv1Location'
v1CreateWebhookRequest:
type: object
properties:
Expand Down
3 changes: 3 additions & 0 deletions server/router/api/v1/memo_service.go
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,9 @@ func (s *APIV1Service) CreateMemo(ctx context.Context, request *v1pb.CreateMemoR
create.Payload = &storepb.MemoPayload{
Property: property,
}
if request.Location != nil {
create.Payload.Location = convertLocationToStore(request.Location)
}

memo, err := s.Store.CreateMemo(ctx, create)
if err != nil {
Expand Down
3 changes: 3 additions & 0 deletions web/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,13 +28,15 @@
"highlight.js": "^11.10.0",
"i18next": "^23.15.1",
"katex": "^0.16.11",
"leaflet": "^1.9.4",
"lodash-es": "^4.17.21",
"lucide-react": "^0.437.0",
"mermaid": "^10.9.1",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-hot-toast": "^2.4.1",
"react-i18next": "^15.0.2",
"react-leaflet": "^4.2.1",
"react-redux": "^9.1.2",
"react-router-dom": "^6.26.2",
"react-use": "^17.5.1",
Expand All @@ -52,6 +54,7 @@
"@types/d3": "^7.4.3",
"@types/dompurify": "^3.0.5",
"@types/katex": "^0.16.7",
"@types/leaflet": "^1.9.12",
"@types/lodash-es": "^4.17.12",
"@types/node": "^22.5.5",
"@types/qs": "^6.9.16",
Expand Down
48 changes: 48 additions & 0 deletions web/pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

46 changes: 46 additions & 0 deletions web/src/components/LeafletMap.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import { LatLng } from "leaflet";
import { useEffect, useState } from "react";
import { MapContainer, Marker, TileLayer, useMapEvents } from "react-leaflet";

interface MarkerProps {
position: LatLng | undefined;
onChange: (position: LatLng) => void;
}

const LocationMarker = (props: MarkerProps) => {
const [position, setPosition] = useState(props.position);

const map = useMapEvents({
click(e) {
setPosition(e.latlng);
map.locate();

// Call the parent onChange function.
props.onChange(e.latlng);
},
locationfound() {},
});

useEffect(() => {
map.attributionControl.setPrefix("");
map.locate();
}, []);

return position === undefined ? null : <Marker position={position}></Marker>;
};

interface MapProps {
latlng?: LatLng;
onChange?: (position: LatLng) => void;
}

const LeafletMap = (props: MapProps) => {
return (
<MapContainer className="w-full h-72" center={props.latlng} zoom={13} scrollWheelZoom={false}>
<TileLayer url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png" attribution="" />
<LocationMarker position={props.latlng} onChange={props.onChange ? props.onChange : () => {}} />
</MapContainer>
);
};

export default LeafletMap;
Original file line number Diff line number Diff line change
Expand Up @@ -151,7 +151,7 @@ const AddMemoRelationPopover = (props: Props) => {
getOptionLabel={(memo) => memo.content}
isOptionEqualToValue={(memo, value) => memo.name === value.name}
renderOption={(props, memo) => (
<AutocompleteOption {...props}>
<AutocompleteOption {...props} key={memo.name}>
<div className="w-full flex flex-col justify-start items-start">
<p className="text-xs text-gray-400 select-none">{memo.displayTime?.toLocaleString()}</p>
<p className="mt-0.5 text-sm leading-5 line-clamp-2">{searchText ? getHighlightedContent(memo.content) : memo.snippet}</p>
Expand Down
116 changes: 116 additions & 0 deletions web/src/components/MemoEditor/ActionButton/LocationSelector.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
import { Button, IconButton, Input } from "@mui/joy";
import { LatLng } from "leaflet";
import { MapPinIcon } from "lucide-react";
import { useEffect, useState } from "react";
import toast from "react-hot-toast";
import LeafletMap from "@/components/LeafletMap";
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/Popover";
import { Location } from "@/types/proto/api/v1/memo_service";
import { useTranslate } from "@/utils/i18n";

interface Props {
location?: Location;
onChange: (location: Location) => void;
}

interface State {
placeholder: string;
position: LatLng;
}

const LocationSelector = (props: Props) => {
const t = useTranslate();
const [state, setState] = useState<State>({
placeholder: props.location?.placeholder || "",
position: new LatLng(props.location?.latitude || 0, props.location?.longitude || 0),
});
const [popoverOpen, setPopoverOpen] = useState<boolean>(false);

useEffect(() => {
if (popoverOpen && !props.location) {
const handleError = (error: any, errorMessage: string) => {
toast.error(errorMessage);
console.error(error);
};

if (navigator.geolocation) {
navigator.geolocation.getCurrentPosition(
(position) => {
const lat = position.coords.latitude;
const lng = position.coords.longitude;
setState({ ...state, position: new LatLng(lat, lng) });
},
(error) => {
handleError(error, "Error getting current position");
},
);
} else {
handleError("Geolocation is not supported by this browser.", "Geolocation is not supported by this browser.");
}
}
}, [popoverOpen]);

useEffect(() => {
// Fetch reverse geocoding data.
fetch(`https://nominatim.openstreetmap.org/reverse?lat=${state.position.lat}&lon=${state.position.lng}&format=json`)
.then((response) => response.json())
.then((data) => {
if (data && data.display_name) {
setState({ ...state, placeholder: data.display_name });
}
})
.catch((error) => {
toast.error("Failed to fetch reverse geocoding data");
console.error("Failed to fetch reverse geocoding data:", error);
});
}, [state.position]);

const onPositionChanged = (position: LatLng) => {
setState({ ...state, position });
};

return (
<Popover open={popoverOpen} onOpenChange={setPopoverOpen}>
<PopoverTrigger>
<IconButton size="sm" component="div">
<MapPinIcon className="w-5 h-5 mx-auto shrink-0" />
{props.location && (
<span className="font-normal ml-0.5 text-ellipsis whitespace-nowrap overflow-hidden max-w-32">
{props.location.placeholder}
</span>
)}
</IconButton>
</PopoverTrigger>
<PopoverContent align="center">
<div className="min-w-80 sm:w-128 flex flex-col justify-start items-start">
<LeafletMap key={JSON.stringify(state.position)} latlng={state.position} onChange={onPositionChanged} />
<div className="mt-2 w-full flex flex-row justify-between items-center gap-2">
<Input
placeholder="Choose location"
value={state.placeholder}
onChange={(e) => setState((state) => ({ ...state, placeholder: e.target.value }))}
/>
<Button
size="sm"
onClick={() => {
props.onChange(
Location.fromPartial({
placeholder: state.placeholder,
latitude: state.position.lat,
longitude: state.position.lng,
}),
);
setPopoverOpen(false);
}}
disabled={!state.position || state.placeholder.length === 0}
>
{t("common.add")}
</Button>
</div>
</div>
</PopoverContent>
</Popover>
);
};

export default LocationSelector;
Loading

0 comments on commit b144faf

Please sign in to comment.