diff --git a/src/assets/icons/selection.json b/src/assets/icons/selection.json index 3d2a6c5..9cbac81 100644 --- a/src/assets/icons/selection.json +++ b/src/assets/icons/selection.json @@ -1,6 +1,29 @@ { "IcoMoonType": "selection", "icons": [ + { + "icon": { + "paths": [ + "M955.077 413.538h-59.077c-15.754 0-29.538-13.785-29.538-29.538v-196.923c0-15.754-13.785-29.538-29.538-29.538h-649.846c-15.754 0-29.538 13.785-29.538 29.538v196.923c0 15.754-13.785 29.538-29.538 29.538h-59.077c-15.754 0-29.538-13.785-29.538-29.538v-265.846c0-43.323 35.446-78.769 78.769-78.769h787.692c43.323 0 78.769 35.446 78.769 78.769v265.846c0 15.754-13.785 29.538-29.538 29.538z", + "M492.308 283.569c11.815-11.815 29.538-11.815 41.354 0l265.846 265.846c11.815 11.815 11.815 29.538 0 41.354l-41.354 41.354c-11.815 11.815-29.538 11.815-41.354 0l-110.277-110.277c-11.815-11.815-33.477-3.938-33.477 13.785v419.446c-1.969 15.754-17.723 29.538-31.508 29.538h-59.077c-15.754 0-29.538-13.785-29.538-29.538v-417.477c0-17.723-21.662-25.6-33.477-13.785l-110.277 110.277c-11.815 11.815-29.538 11.815-41.354 0l-41.354-43.323c-11.815-11.815-11.815-29.538 0-41.354l265.846-265.846z" + ], + "attrs": [{}, {}], + "isMulticolor": false, + "isMulticolor2": false, + "grid": 0, + "tags": ["upload"] + }, + "attrs": [{}, {}], + "properties": { + "order": 38, + "id": 21, + "name": "upload", + "prevSize": 32 + }, + "setIdx": 3, + "setId": 2, + "iconIdx": 0 + }, { "icon": { "paths": [ diff --git a/src/components/icon/constants.ts b/src/components/icon/constants.ts index 530ae27..aa7fc75 100644 --- a/src/components/icon/constants.ts +++ b/src/components/icon/constants.ts @@ -28,4 +28,5 @@ export enum IconIdentifier { Tick = 'tick', TileLayer = 'tiled-layer', VectorLayer = 'vector-layer', + Upload = 'upload', } diff --git a/src/components/input/file-input.tsx b/src/components/input/file-input.tsx new file mode 100644 index 0000000..81512be --- /dev/null +++ b/src/components/input/file-input.tsx @@ -0,0 +1,103 @@ +import React, { useCallback, useState } from 'react'; +import classNames from 'classnames'; +import { Icon, IconIdentifier } from '../icon'; + +interface FileInputProps { + accept?: string; + onChange?: (file: File) => void; + className?: string; + error?: boolean; +} + +export const FileInput: React.FC = ({ + accept, + onChange, + className, + error, +}) => { + const [isDragging, setIsDragging] = useState(false); + const [fileName, setFileName] = useState(); + + const handleFile = useCallback( + (file: File) => { + setFileName(file.name); + onChange?.(file); + }, + [onChange], + ); + + const handleDrop = useCallback( + (e: React.DragEvent) => { + e.preventDefault(); + setIsDragging(false); + + const file = e.dataTransfer.files?.[0]; + if (file) { + handleFile(file); + } + }, + [handleFile], + ); + + const handleDragOver = useCallback((e: React.DragEvent) => { + e.preventDefault(); + setIsDragging(true); + }, []); + + const handleDragLeave = useCallback((e: React.DragEvent) => { + e.preventDefault(); + setIsDragging(false); + }, []); + + const handleChange = useCallback( + (e: React.ChangeEvent) => { + const file = e.target.files?.[0]; + if (file) { + handleFile(file); + } + }, + [handleFile], + ); + + return ( +
+ +
+ + {fileName ? ( +
{fileName}
+ ) : ( + <> +
+ Drop your file here, or{' '} + browse +
+
+ Supports GeoJSON files (.geojson, .json) +
+ + )} +
+
+ ); +}; diff --git a/src/components/input/index.ts b/src/components/input/index.ts index 04f7906..ccf2ceb 100644 --- a/src/components/input/index.ts +++ b/src/components/input/index.ts @@ -1,2 +1,3 @@ export * from './input'; export * from './types'; +export * from './file-input'; diff --git a/src/layer-panel/add-layer.tsx b/src/layer-panel/add-raster-layer.tsx similarity index 98% rename from src/layer-panel/add-layer.tsx rename to src/layer-panel/add-raster-layer.tsx index 05d775c..0ab9055 100644 --- a/src/layer-panel/add-layer.tsx +++ b/src/layer-panel/add-raster-layer.tsx @@ -19,7 +19,7 @@ type FormData = { southLatitude: number; }; -export const AddLayerModal: React.FC = ({ +export const AddRasterLayerModal: React.FC = ({ show, onClose, }) => { diff --git a/src/layer-panel/add-vector-layer.tsx b/src/layer-panel/add-vector-layer.tsx new file mode 100644 index 0000000..cb5e9b2 --- /dev/null +++ b/src/layer-panel/add-vector-layer.tsx @@ -0,0 +1,165 @@ +import { Fieldset } from '@headlessui/react'; +import React, { useCallback, useContext } from 'react'; +import { useForm } from 'react-hook-form'; +import { Button, IconIdentifier, Input, Modal, FileInput } from '../components'; +import { GlobalContext } from '../contexts'; +import { DEFAULT_STYLES } from './draw/constants'; + +type AddLayerModalProps = { + onClose: VoidFunction; + show: boolean; +}; + +type FormData = { + name: string; + geojsonData: string; + file?: FileList; +}; + +export const AddVectorLayerModal: React.FC = ({ + show, + onClose, +}) => { + const { layerManager } = useContext(GlobalContext); + const { + register, + handleSubmit, + reset, + setError, + formState: { errors }, + } = useForm(); + + const handleFileUpload = useCallback( + (file: File) => { + const reader = new FileReader(); + reader.onload = (e) => { + try { + const content = e.target?.result as string; + const geojson = JSON.parse(content); + + layerManager?.addGeoJsonLayer({ + name: file.name.replace(/\.[^/.]+$/, ''), // Remove file extension + data: geojson, + styles: DEFAULT_STYLES, + }); + + reset(); + onClose(); + } catch { + setError('file', { + type: 'validate', + message: 'Invalid GeoJSON format', + }); + } + }; + reader.readAsText(file); + }, + [layerManager, onClose, reset, setError], + ); + + const onSubmit = (data: FormData) => { + try { + const geojson = JSON.parse(data.geojsonData); + layerManager?.addGeoJsonLayer({ + name: data.name, + data: geojson, + styles: DEFAULT_STYLES, + }); + reset(); + onClose(); + } catch { + setError('geojsonData', { + type: 'validate', + message: 'Invalid GeoJSON format', + }); + } + }; + + return ( + +
+ {/* Basic Info */} +
Basic Info
+ + + {/* GeoJSON Data */} +
GeoJSON Data
+ + {/* File Upload */} + + + {/* Divider with OR */} +
+
+
+
+
+ + OR + +
+
+ + {/* Text Input */} +
+ +