Skip to content

Commit

Permalink
create grid functionality
Browse files Browse the repository at this point in the history
  • Loading branch information
Codebmk committed Jan 30, 2025
1 parent c8d7198 commit c9999a0
Show file tree
Hide file tree
Showing 8 changed files with 230 additions and 26 deletions.
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
"use client";

import { useState } from "react";
import { useRouter } from "next/navigation";
import { zodResolver } from "@hookform/resolvers/zod";
import { useForm } from "react-hook-form";
import * as z from "zod";
Expand All @@ -26,7 +25,7 @@ import {
DialogTrigger,
} from "@/components/ui/dialog";
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
import { Loader2, MapPin, Check } from "lucide-react";
import { Loader2, Check } from "lucide-react";
import { cn } from "@/lib/utils";
import { sites } from "@/core/apis/sites";
import { useAppSelector } from "@/core/redux/hooks";
Expand All @@ -35,8 +34,6 @@ import { MapContainer, TileLayer, Marker, useMap } from "react-leaflet";
import "leaflet/dist/leaflet.css";
import L from "leaflet";
import { useApproximateCoordinates } from "@/core/hooks/useSites";
import { AxiosError } from "axios";
import Error from "next/error";

const siteFormSchema = z.object({
name: z.string().min(2, {
Expand Down
3 changes: 2 additions & 1 deletion netmanager-app/app/types/grids.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import { Position } from "@/core/redux/slices/gridsSlice";
import { Site } from "./sites";

export interface CreateGrid {
name: string;
admin_level: string;
shape: {
type: "MultiPolygon" | "Polygon";
coordinates: number[][][][];
coordinates: Position[][] | Position[][][];
};
network: string;
}
Expand Down
57 changes: 47 additions & 10 deletions netmanager-app/components/grids/create-grid.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,15 @@ import {
import { Input } from "@/components/ui/input";
import { Textarea } from "@/components/ui/textarea";
import { Plus } from "lucide-react";
import PolygonMap from "./polymap";
import { useAppSelector } from "@/core/redux/hooks";
import { useCreateGrid } from "@/core/hooks/useGrids";
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
import { AxiosError } from "axios";

interface ErrorResponse {
message: string;
}

const gridFormSchema = z.object({
name: z.string().min(2, {
Expand All @@ -36,26 +45,50 @@ const gridFormSchema = z.object({
shapefile: z.string().min(2, {
message: "Shapefile data is required.",
}),
network: z.string().min(2, {
message: "Grid name must be at least 2 characters.",
}),
});

type GridFormValues = z.infer<typeof gridFormSchema>;

export function CreateGridForm() {
const [open, setOpen] = useState(false);
const [error, setError] = useState<string | null>(null);
const polygon = useAppSelector((state) => state.grids.polygon);
const activeNetwork = useAppSelector((state) => state.user.activeNetwork);
const { createGrid, isLoading } = useCreateGrid();

const form = useForm<GridFormValues>({
resolver: zodResolver(gridFormSchema),
defaultValues: {
name: "",
administrativeLevel: "",
shapefile: '{"type":"","coordinates":[]}',
network: activeNetwork?.net_name,
},
});

function onSubmit(data: GridFormValues) {
console.log(data);
setOpen(false);
}
const onSubmit = async (data: GridFormValues) => {
try {
if (!polygon) {
setError("Shapefile is required");
return;
}
const gridData = {
name: data.name,
admin_level: data.administrativeLevel,
shape: polygon,
network: activeNetwork?.net_name || "",
};

await createGrid(gridData);

setOpen(false);
} catch (error: AxiosError<ErrorResponse>) {
setError(error.message || "An error occurred while creating the site.");
}
};

return (
<Dialog open={open} onOpenChange={setOpen}>
Expand Down Expand Up @@ -114,7 +147,9 @@ export function CreateGridForm() {
<Textarea
placeholder='{"type":"","coordinates":[]}'
className="font-mono"
disabled
{...field}
value={JSON.stringify(polygon)}
/>
</FormControl>
<FormDescription>
Expand All @@ -129,19 +164,21 @@ export function CreateGridForm() {
type="button"
variant="outline"
onClick={() => setOpen(false)}
disabled={isLoading}
>
Cancel
</Button>
<Button type="submit">Submit</Button>
</div>
{error && (
<Alert variant="destructive">
<AlertTitle>Error</AlertTitle>
<AlertDescription>{error}</AlertDescription>
</Alert>
)}
</form>
</Form>
<div className="h-[400px] bg-muted rounded-md">
{/* Map component would go here - using placeholder for now */}
<div className="w-full h-full flex items-center justify-center text-muted-foreground">
Map Component
</div>
</div>
<PolygonMap />
</div>
</DialogContent>
</Dialog>
Expand Down
120 changes: 120 additions & 0 deletions netmanager-app/components/grids/polymap.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
import React, { useEffect } from "react";
import L from "leaflet";
import { MapContainer, TileLayer, FeatureGroup } from "react-leaflet";
import { EditControl } from "react-leaflet-draw";
import "leaflet/dist/leaflet.css";
import "leaflet-draw/dist/leaflet.draw.css";
import { useDispatch } from "react-redux";
import { Position, setPolygon } from "@/core/redux/slices/gridsSlice";

// Extend Leaflet types to include _getIconUrl
interface IconDefault extends L.Icon {
_getIconUrl?: string;
}

// Type for polygon data structure
interface PolygonData {
type: string;
coordinates: Position[][] | Position[][][];
}

// Type for draw control options
interface DrawControlOptions {
rectangle: boolean;
circle: boolean;
circlemarker: boolean;
polyline: boolean;
marker: boolean;
}

// Type for draw events
interface DrawEvent {
layerType: string;
layer: L.Layer;
}

// Type for edit events
interface EditEvent {
layers: L.LayerGroup;
}

// Fix Leaflet's default icon issue
delete (L.Icon.Default.prototype as IconDefault)._getIconUrl;
L.Icon.Default.mergeOptions({
iconRetinaUrl:
"https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.9.4/images/marker-icon-2x.png",
iconUrl:
"https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.9.4/images/marker-icon.png",
shadowUrl:
"https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.9.4/images/marker-shadow.png",
});

const PolygonMap: React.FC = () => {
const dispatch = useDispatch();
const ZOOM_LEVEL = 10;

const drawOptions: DrawControlOptions = {
rectangle: false,
circle: false,
circlemarker: false,
polyline: false,
marker: false,
};

const _created = (e: DrawEvent): void => {
const { layerType, layer } = e;
if (layerType === "polygon") {
const polygon = layer as L.Polygon;
const geoJson = polygon.toGeoJSON();
dispatch(
setPolygon({
type: geoJson.geometry.type,
coordinates: geoJson.geometry.coordinates,
})
);
}
};

const _edited = (e: EditEvent): void => {
const { layers } = e;
layers.eachLayer((layer: L.Layer) => {
if (layer instanceof L.Polygon) {
const geoJson = layer.toGeoJSON();
dispatch(
setPolygon({
type: geoJson.geometry.type,
coordinates: geoJson.geometry.coordinates,
})
);
}
});
};

return (
<div className="h-[400px] w-full bg-muted rounded-md overflow-hidden opacity-80">
<div className="w-full h-full relative">
<MapContainer
center={{ lat: 0.347596, lng: 32.58252 }}
zoom={ZOOM_LEVEL}
className="h-full w-full"
scrollWheelZoom={false}
>
<FeatureGroup>
<EditControl
position="topright"
onCreated={_created}
onEdited={_edited}
draw={drawOptions}
/>
</FeatureGroup>
<TileLayer
attribution='&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
/>
</MapContainer>
</div>
</div>
);
};

export default PolygonMap;
4 changes: 2 additions & 2 deletions netmanager-app/core/hooks/useGrids.ts
Original file line number Diff line number Diff line change
Expand Up @@ -96,15 +96,15 @@ export const useCreateGrid = () => {
await grids.createGridApi(newGrid),
onSuccess: () => {
// Invalidate and refetch the grid summary after creating a new grid
queryClient.invalidateQueries({ queryKey: ["gridSummary"] });
queryClient.invalidateQueries({ queryKey: ["grids"] });
},
onError: (error: AxiosError<ErrorResponse>) => {
console.error("Failed to create grid:", error.response?.data?.message);
},
});

return {
createGrid: mutation.mutate,
createGrid: mutation.mutateAsync,
isLoading: mutation.isPending,
error: mutation.error,
};
Expand Down
21 changes: 20 additions & 1 deletion netmanager-app/core/redux/slices/gridsSlice.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,27 @@ import { createSlice } from "@reduxjs/toolkit";
import type { PayloadAction } from "@reduxjs/toolkit";
import { Grid } from "@/app/types/grids";

export type Position = [number, number]; // Represents a coordinate as [longitude, latitude]

export interface GridsState {
grids: Grid[];
activeGrid: Grid[] | null;
isLoading: boolean;
error: string | null;
polygon: {
type: "MultiPolygon" | "Polygon";
coordinates: Position[][] | Position[][][] | null;
};
}
const initialState: GridsState = {
grids: [],
activeGrid: null,
isLoading: false,
error: null,
polygon: {
type: "Polygon",
coordinates: null,
},
};

const gridsSlice = createSlice({
Expand All @@ -34,9 +44,18 @@ const gridsSlice = createSlice({
state.error = action.payload;
state.isLoading = false;
},
setPolygon(
state,
action: PayloadAction<{
type: "MultiPolygon" | "Polygon";
coordinates: Position[][] | Position[][][] | null;
}>
) {
state.polygon = action.payload;
},
},
});

export const { setGrids, setActiveCohort, setLoading, setError } =
export const { setGrids, setActiveCohort, setLoading, setError, setPolygon } =
gridsSlice.actions;
export default gridsSlice.reducer;
Loading

0 comments on commit c9999a0

Please sign in to comment.