From 391810811ae1a4a0e27a64cf4a5355c8faa9de47 Mon Sep 17 00:00:00 2001 From: Chris Carlon Date: Sun, 27 Oct 2024 22:23:15 +0000 Subject: [PATCH] feat(UI and Backend): Started implementing file upload logic [2024-10-27] --- gridwalk-ui/src/app/api/upload/layer/route.ts | 61 +++++++++++ .../components/navBars/mainMapNavigation.tsx | 103 ++++++++++++++++-- .../app/project/components/navBars/types.ts | 15 ++- gridwalk-ui/src/app/project/page.tsx | 71 +++++++++--- 4 files changed, 221 insertions(+), 29 deletions(-) create mode 100644 gridwalk-ui/src/app/api/upload/layer/route.ts diff --git a/gridwalk-ui/src/app/api/upload/layer/route.ts b/gridwalk-ui/src/app/api/upload/layer/route.ts new file mode 100644 index 0000000..58d48c1 --- /dev/null +++ b/gridwalk-ui/src/app/api/upload/layer/route.ts @@ -0,0 +1,61 @@ +import { NextRequest } from "next/server"; + +// Hardcoded auth token - replace with your actual token +const HARDCODED_AUTH_TOKEN = ""; + +export async function POST(request: NextRequest) { + try { + const formData = await request.formData(); + const file = formData.get("file"); + + if (!file || !(file instanceof File)) { + return new Response( + JSON.stringify({ + success: false, + error: "No file provided", + }), + { status: 400 }, + ); + } + + // Create a new FormData for the API call + const apiFormData = new FormData(); + apiFormData.append("file", file); + + // Hardcoded values + apiFormData.append("workspace_id", ""); + apiFormData.append("name", "my-awesome-layer"); + + const response = await fetch("http://localhost:3001/upload_layer", { + method: "POST", + headers: { + Authorization: `Bearer ${HARDCODED_AUTH_TOKEN}`, + }, + body: apiFormData, + }); + + if (!response.ok) { + throw new Error("Upload failed"); + } + + const data = await response.json(); + return new Response(JSON.stringify({ success: true, data }), { + status: 200, + }); + } catch (error) { + console.error("Upload error:", error); + return new Response( + JSON.stringify({ + success: false, + error: "Upload failed", + }), + { status: 500 }, + ); + } +} + +export const config = { + api: { + bodyParser: false, + }, +}; diff --git a/gridwalk-ui/src/app/project/components/navBars/mainMapNavigation.tsx b/gridwalk-ui/src/app/project/components/navBars/mainMapNavigation.tsx index 4dce68f..ef7569a 100644 --- a/gridwalk-ui/src/app/project/components/navBars/mainMapNavigation.tsx +++ b/gridwalk-ui/src/app/project/components/navBars/mainMapNavigation.tsx @@ -1,16 +1,26 @@ import React from "react"; -import { Map, Layers, Settings, Info, File, X } from "lucide-react"; +import { + Map, + Layers, + Settings, + Info, + File, + X, + Upload, + Trash2, +} from "lucide-react"; import { ModalProps, MainMapNav } from "./types"; -{ - /* Create NavBar map Modal */ -} const MapModal: React.FC = ({ isOpen, onClose, onNavItemClick, selectedItem, - children, + layers, + onLayerUpload, + onLayerDelete, + isUploading, + error, }) => { const MainMapNavs: MainMapNav[] = [ { @@ -45,6 +55,13 @@ const MapModal: React.FC = ({ }, ]; + const handleFileChange = (event: React.ChangeEvent) => { + const file = event.target.files?.[0]; + if (file) { + onLayerUpload(file); + } + }; + const getIconComponent = (iconName: string) => { switch (iconName) { case "map": @@ -62,6 +79,76 @@ const MapModal: React.FC = ({ } }; + const renderModalContent = () => { + if (!selectedItem) return null; + + switch (selectedItem.id) { + case "upload": + return ( +
+

Upload

+ + {/* Upload Section */} +
+ +
+ + {/* Status Messages */} + {isUploading && ( +
Uploading...
+ )} + {error &&
{error}
} + + {/* Layers List */} +
+ {layers.length === 0 ? ( +

No layers uploaded yet

+ ) : ( + layers.map((layer) => ( +
+
+

{layer.name}

+

{layer.type}

+
+ +
+ )) + )} +
+
+ ); + + default: + return ( +
+

{selectedItem.title}

+

{selectedItem.description}

+
+ ); + } + }; + return ( <> {/* Navigation Bar */} @@ -112,11 +199,7 @@ const MapModal: React.FC = ({ {/* Content with black text override */}
-
- {" "} - {/* Force black text for content */} - {children} -
+
{renderModalContent()}
diff --git a/gridwalk-ui/src/app/project/components/navBars/types.ts b/gridwalk-ui/src/app/project/components/navBars/types.ts index cda9dfc..de0bd26 100644 --- a/gridwalk-ui/src/app/project/components/navBars/types.ts +++ b/gridwalk-ui/src/app/project/components/navBars/types.ts @@ -4,6 +4,7 @@ export interface MainMapNav { title: string; icon?: string; description?: string; + children?: React.ReactNode; } /* Simple Modal Prop Elements */ @@ -12,7 +13,11 @@ export interface ModalProps { onClose: () => void; onNavItemClick: (item: MainMapNav) => void; selectedItem: MainMapNav | null; - children: React.ReactNode; + layers: LayerUpload[]; + onLayerUpload: (file: File) => Promise; + onLayerDelete: (layerId: string) => void; + isUploading: boolean; + error: string | null; } /* Map Edit Items */ @@ -30,3 +35,11 @@ export interface BaseEditNav { icon?: string; description: string; } + +/* Upload Layer */ +export interface LayerUpload { + id: string; + name: string; + type: string; + visible?: boolean; +} diff --git a/gridwalk-ui/src/app/project/page.tsx b/gridwalk-ui/src/app/project/page.tsx index dfa134b..cc96ffb 100644 --- a/gridwalk-ui/src/app/project/page.tsx +++ b/gridwalk-ui/src/app/project/page.tsx @@ -2,7 +2,7 @@ import React, { useState } from "react"; import { useMapInit } from "./components/mapInit/mapInit"; import MainMapNavigation from "./components/navBars/mainMapNavigation"; -import { MainMapNav } from "./components/navBars/types"; +import { MainMapNav, LayerUpload } from "./components/navBars/types"; import MapEditNavigation from "./components/navBars/mapEditNavigation"; import { MapEditNav } from "./components/navBars/types"; import BaseLayerNavigation from "./components/navBars/baseLayerNavigation"; @@ -21,7 +21,6 @@ const INITIAL_MAP_CONFIG = { zoom: 11, }; -// Set up page export default function Project() { // State management const [selectedItem, setSelectedItem] = useState(null); @@ -34,13 +33,58 @@ export default function Project() { const [isModalOpen, setIsModalOpen] = useState(false); const [currentStyle, setCurrentStyle] = useState(MAP_STYLES.light); + // Layer state management + const [layers, setLayers] = useState([]); + const [isUploading, setIsUploading] = useState(false); + const [error, setError] = useState(null); + // Map initialisation const { mapContainer, mapError } = useMapInit({ ...INITIAL_MAP_CONFIG, styleUrl: currentStyle, }); - // Event handlers + // Layer handlers + const handleLayerUpload = async (file: File) => { + setIsUploading(true); + setError(null); + + try { + const formData = new FormData(); + formData.append("file", file); + + const response = await fetch("/api/upload/layer", { + method: "POST", + body: formData, + }); + + if (!response.ok) { + throw new Error("Upload failed"); + } + + const data = await response.json(); + + setLayers((prev) => [ + ...prev, + { + id: data.data.id || Math.random().toString(), + name: file.name, + type: file.type, + visible: true, + }, + ]); + } catch (err) { + setError(err instanceof Error ? err.message : "Failed to upload file"); + } finally { + setIsUploading(false); + } + }; + + const handleLayerDelete = (layerId: string) => { + setLayers((prev) => prev.filter((layer) => layer.id !== layerId)); + }; + + // Navigation handlers const handleNavItemClick = (item: MainMapNav) => { setSelectedItem(item); setIsModalOpen(true); @@ -72,38 +116,29 @@ export default function Project() { return (
- {/* Error display */} {mapError && (
{mapError}
)} - - {/* Map container */}
- - {/* Navigation components */} - - {selectedItem && ( -
-

{selectedItem.title}

-

{selectedItem.description}

-
- )} -
- + layers={layers} + onLayerUpload={handleLayerUpload} + onLayerDelete={handleLayerDelete} + isUploading={isUploading} + error={error} + />