From 8a3c18896e68458e745269db51f526942e6ce8b5 Mon Sep 17 00:00:00 2001 From: DmsAnhr Date: Mon, 22 Dec 2025 15:26:37 +0700 Subject: [PATCH] add new components --- src/components/MetaDataForm.jsx | 186 ++++++-- src/components/common/ConfirmDialog.jsx | 48 ++ src/components/common/MultiSelect.jsx | 105 +++++ src/components/layers_preview/GeoPreview.jsx | 117 +++++ .../layers_style/CustomLayerStyle.jsx | 415 ++++++++++++++++++ src/components/layers_style/StylePreview.jsx | 187 ++++++++ src/components/layers_style/StylingLayers.jsx | 157 +++++++ src/components/ui/alert-dialog.jsx | 136 ++++++ src/components/ui/button.jsx | 58 +++ src/components/ui/checkbox.jsx | 28 ++ src/components/ui/popover.jsx | 45 ++ src/components/ui/sheet.jsx | 138 ++++++ src/components/ui/tabs.jsx | 2 +- src/components/upload/FilePreview.jsx | 8 +- 14 files changed, 1595 insertions(+), 35 deletions(-) create mode 100644 src/components/common/ConfirmDialog.jsx create mode 100644 src/components/common/MultiSelect.jsx create mode 100644 src/components/layers_preview/GeoPreview.jsx create mode 100644 src/components/layers_style/CustomLayerStyle.jsx create mode 100644 src/components/layers_style/StylePreview.jsx create mode 100644 src/components/layers_style/StylingLayers.jsx create mode 100644 src/components/ui/alert-dialog.jsx create mode 100644 src/components/ui/button.jsx create mode 100644 src/components/ui/checkbox.jsx create mode 100644 src/components/ui/popover.jsx create mode 100644 src/components/ui/sheet.jsx diff --git a/src/components/MetaDataForm.jsx b/src/components/MetaDataForm.jsx index 4549952..94b6e0b 100644 --- a/src/components/MetaDataForm.jsx +++ b/src/components/MetaDataForm.jsx @@ -6,6 +6,7 @@ import { TabsTrigger, TabsContent, } from "./ui/tabs"; +import MultiSelect from "./common/MultiSelect"; /** * 📄 MetadataForm.jsx @@ -13,15 +14,74 @@ import { * Menggunakan Tailwind CSS murni untuk tampilan modern dan profesional. */ -export default function MetadataForm({ onChange }) { +export default function MetadataForm({ onChange, initialValues = {} }) { + const today = new Date().toISOString().split('T')[0]; + + // const [formData, setFormData] = useState({ + // // 🧩 Identifikasi Dataset + // title: "", + // abstract: "", + // keywords: "", + // topicCategory: "", + // dateCreated: "", + // status: "", + // language: "eng", + + // // 🧭 Referensi Spasial + // crs: "EPSG:4326", + // geometryType: "", + // xmin: "", + // xmax: "", + // ymin: "", + // ymax: "", + + // // 🌐 Distribusi / Akses Data + // downloadLink: "", + // serviceLink: "", + // format: "", + // license: "Copyright", + + // // 👤 Informasi Penanggung Jawab + // organization: "", + // contactName: "", + // contactEmail: "", + // contactPhone: "", + // role: "", + + // // 🧾 Metadata Umum + // metadataStandard: "ISO 19115:2003/19139", + // metadataVersion: "1.0", + // metadataUUID: "", + // metadataDate: "", + // charset: "utf8", + // rsIdentifier: "WGS 1984" + // }); + + // Generate UUID & tanggal metadata saat pertama kali load + + + + + + + + + + + const [formData, setFormData] = useState({ // 🧩 Identifikasi Dataset - title: "", - abstract: "", - keywords: "", - topicCategory: "", - dateCreated: "", - status: "", + // title: "test_upload_geos", + // title: "", + title: initialValues.judul?initialValues.judul:"", + // abstract: "hanya test", + abstract: initialValues.abstrak?initialValues.abstrak:"", + // keywords: "tets, demo, gatau", + keywords: initialValues.keyword?initialValues.keyword.join(', '):"", + topicCategory: initialValues.kategori?initialValues.kategori:"", + mapsetCategory: "019a0997-5b42-7c34-9ab8-35b4765ecb39", + dateCreated: today, + status: "completed", language: "eng", // 🧭 Referensi Spasial @@ -39,10 +99,10 @@ export default function MetadataForm({ onChange }) { license: "Copyright", // 👤 Informasi Penanggung Jawab - organization: "", - contactName: "", - contactEmail: "", - contactPhone: "", + organization: "PUPR", + contactName: "Dimas", + contactEmail: "pu@gmail.com", + contactPhone: "08222222222", role: "", // 🧾 Metadata Umum @@ -53,8 +113,8 @@ export default function MetadataForm({ onChange }) { charset: "utf8", rsIdentifier: "WGS 1984" }); - - // Generate UUID & tanggal metadata saat pertama kali load + + useEffect(() => { setFormData((prev) => ({ ...prev, @@ -78,14 +138,14 @@ export default function MetadataForm({ onChange }) { {/* TAB LIST */} - + {/* 🧩 Identifikasi Dataset 👤 Penanggung Jawab - + */} {/* TAB 1: IDENTIFIKASI */} @@ -111,35 +171,84 @@ export default function MetadataForm({ onChange }) { onChange={handleChange} /> - + + {/* + /> */} - setSelectedStyle(e.target.value)} + > + + + {/* */} + {/* */} + {geometryType === "Point" && } + + + + {/* ---------------- SINGLE COLOR ---------------- */} + {selectedStyle === "single" && ( +
+ + setSingleColor(e.target.value)} + /> +
+ )} + + {/* ---------------- UNIQUE VALUE ---------------- */} + {selectedStyle === "unique_value" && ( +
+ + + + {/* RULE LIST */} +
+ {uniqueRules.map((r, i) => ( +
+
{r.value}
+ { + const copy = [...uniqueRules]; + copy[i].color = e.target.value; + setUniqueRules(copy); + }} + /> +
+ ))} +
+
+ )} + + {/* ---------------- RANDOM COLOR ---------------- */} + {selectedStyle === "random" && ( +
+ + + {randomRules.map((r, i) => ( +
+ ID {r.id} + { + const copy = [...randomRules]; + copy[i].color = e.target.value; + setRandomRules(copy); + }} + /> +
+ ))} +
+ )} + + {/* ---------------- PROPORTIONAL ---------------- */} + {selectedStyle === "proportional" && ( +
+ + + +
+ + setPropMin(Number(e.target.value))} + /> +
+ +
+ + setPropMax(Number(e.target.value))} + /> +
+
+ )} + + {/* ---------------- ICON PER FEATURE ---------------- */} + {selectedStyle === "icon" && geometryType === "Point" && ( +
+ + setIconGlobal(e.target.value)} + /> +
+ )} + +
+ +
+ + + ); +}; + +export default CustomLayerStyle; diff --git a/src/components/layers_style/StylePreview.jsx b/src/components/layers_style/StylePreview.jsx new file mode 100644 index 0000000..b8e92f7 --- /dev/null +++ b/src/components/layers_style/StylePreview.jsx @@ -0,0 +1,187 @@ +import React, { useEffect, useRef } from "react"; +import Map from "ol/Map"; +import View from "ol/View"; +import VectorLayer from "ol/layer/Vector"; +import VectorSource from "ol/source/Vector"; +import TileLayer from "ol/layer/Tile"; +import OSM from "ol/source/OSM"; +import WKT from "ol/format/WKT"; +import Feature from "ol/Feature"; + +import Style from "ol/style/Style"; +import Fill from "ol/style/Fill"; +import Stroke from "ol/style/Stroke"; +import CircleStyle from "ol/style/Circle"; + +import SldStyleParser from "geostyler-sld-parser"; +import OlStyleParser from "geostyler-openlayers-parser"; +import { defaults as defaultControls } from 'ol/control'; + +// ============================= +// GEOMETRY +// ============================= +const wkt = new WKT(); + +function parseWKT(str) { + try { + return wkt.readGeometry(str, { + dataProjection: "EPSG:4326", + featureProjection: "EPSG:3857", + }); + } catch (e) { + console.error("WKT Error:", str); + return null; + } +} + +function normalizeKey(key) { + return key.replace(/[^a-zA-Z0-9_]/g, "_"); +} + +function createFeatures(data) { + return data.map((row) => { + const geometry = parseWKT(row.geometry); + const feat = new Feature(); + + Object.entries(row).forEach(([key, value]) => { + if (key === "geometry") return; + + // ORIGINAL KEY (untuk SLD / GeoStyler) + feat.set(key, value); + + // NORMALIZED KEY (untuk sistem kamu) + const normalized = normalizeKey(key).toUpperCase(); + // console.log(normalized, key); + if (normalized !== key) { + feat.set(normalized, value); + } + + }); + + feat.setGeometry(geometry); + return feat; + }); +} + + + +// ============================= +// FALLBACK STYLE +// ============================= +const defaultStyle = new Style({ + image: new CircleStyle({ + radius: 6, + fill: new Fill({ color: "#3388ff" }), + stroke: new Stroke({ color: "#333", width: 1 }), + }), + stroke: new Stroke({ color: "#3388ff", width: 2 }), + fill: new Fill({ color: "rgba(51,136,255,0.5)" }), +}); + + +// ============================= +// PREVIEW MAP (GEOSTYLER) +// ============================= +const SpatialStylePreviewGeoStyler = ({ data, styleConfig }) => { + const mapRef = useRef(null); + const mapObj = useRef(null); + const vectorLayer = useRef(null); + + // init map + useEffect(() => { + if (!mapRef.current) return; + + const features = createFeatures(data); + + const vectorSource = new VectorSource({ features }); + vectorLayer.current = new VectorLayer({ + source: vectorSource, + style: defaultStyle, + }); + + mapObj.current = new Map({ + target: mapRef.current, + controls: defaultControls({ + attribution: false, // ⬅️ ini kuncinya + zoom: true, + }), + layers: [ + new TileLayer({ source: new OSM() }), + vectorLayer.current, + ], + view: new View({ + center: [12600000, -830000], + zoom: 7, + }), + }); + + return () => mapObj.current?.setTarget(null); + }, [data]); + + + // ============================= + // APPLY SLD VIA GEOSTYLER + // ============================= + useEffect(() => { + if (!vectorLayer.current) return; + if (!styleConfig || styleConfig.styleType !== "sld") { + vectorLayer.current.setStyle(defaultStyle); + return; + } + + const applySLD = async () => { + try { + const sldParser = new SldStyleParser(); + const olParser = new OlStyleParser(); + + // 1️⃣ SLD XML → GeoStyler Style + const sldResult = await sldParser.readStyle( + styleConfig.sldContent + ); + + const geoStyle = sldResult.output; + + if (!geoStyle) { + throw new Error("GeoStyler returned empty style"); + } + + // 2️⃣ GeoStyler Style → OL Style / StyleFunction + const olResult = await olParser.writeStyle(geoStyle); + const olStyle = olResult.output; + + // 3️⃣ APPLY STYLE (TYPE SAFE) + if (typeof olStyle === "function") { + vectorLayer.current.setStyle((feature, resolution) => + olStyle(feature, resolution) + ); + } else { + vectorLayer.current.setStyle(olStyle); + } + + // 4️⃣ FORCE REDRAW + vectorLayer.current.getSource().changed(); + vectorLayer.current.changed(); + + } catch (err) { + console.warn("SLD parsing failed, fallback used", err); + vectorLayer.current.setStyle(defaultStyle); + } + }; + + + + applySLD(); + }, [styleConfig]); + + + return ( +
+
+
+ ); +}; + +export default SpatialStylePreviewGeoStyler; diff --git a/src/components/layers_style/StylingLayers.jsx b/src/components/layers_style/StylingLayers.jsx new file mode 100644 index 0000000..bfb4586 --- /dev/null +++ b/src/components/layers_style/StylingLayers.jsx @@ -0,0 +1,157 @@ +import React, {useState} from "react"; +import { Tabs, TabsList, TabsTrigger, TabsContent } from "../ui/tabs"; +import CustomLayerStyle from "./CustomLayerStyle"; // komponen yang sudah kamu buat + +function normalizeBase64(xmlString) { + return xmlString.replace( + /xlink:href="base64:([^"?]+)(\?[^"]*)?"/g, + (_, base64Content) => { + return `xlink:href="data:image/svg+xml;base64,${base64Content}"`; + } + ); +} + + +const StylingLayers = ({ data, geometryType, onSubmit, geosStyle, changeGeos=null }) => { + const [activeTab, setActiveTab] = useState("custom"); + const [customStyle, setCustomStyle] = useState(null); + const [uploadStyle, setUploadStyle] = useState(null); + const [geosStyleValue, setGeosStyleValue] = useState(null); + + const [parsedSld, setParsedSld] = useState(null); + + const handleSubmit = () => { + if (activeTab === "custom") onSubmit(customStyle); + if (activeTab === "upload") onSubmit(uploadStyle); + if (activeTab === "geoserver") onSubmit(geosStyleValue); + }; + + return ( +
+ + + {/* TAB LIST */} + + Custom Styling + Upload SLD + {/* Ambil dari GeoServer */} + + + {/* ---------------------------------------------------- */} + {/* TAB 1 : CUSTOM STYLING */} + {/* ---------------------------------------------------- */} + + + + + {/* ---------------------------------------------------- */} + {/* TAB 2 : UPLOAD SLD */} + {/* ---------------------------------------------------- */} + +
+
+

Upload File SLD

+ +

+ Unggah file .sld untuk mengganti style layer. +

+ + { + const file = e.target.files?.[0]; + const reader = new FileReader(); + + reader.onload = () => { + const sld = normalizeBase64(reader.result) + setParsedSld(sld) + onSubmit({ + styleType: "sld", + sldContent: sld, + }); + }; + + reader.readAsText(file); + }} + /> +
+
+ +
+
+
+ + {/* ---------------------------------------------------- */} + {/* TAB 3 : STYLE DARI GEOSERVER */} + {/* ---------------------------------------------------- */} + +
+

Ambil Style dari GeoServer

+ + {/*

+ Masukkan nama workspace dan style di GeoServer. +

+
+
+ + + onSubmit({ + styleType: "from_geoserver", + workspace: e.target.value, + }) + } + /> +
+
+ + + onSubmit({ + styleType: "from_geoserver", + styleName: e.target.value, + }) + } + /> +
+
*/} + {geosStyle.map((item, i) => ( +
+ {item.name} +
+ ))} +
+
+ +
+
+ ); +}; + +export default StylingLayers; diff --git a/src/components/ui/alert-dialog.jsx b/src/components/ui/alert-dialog.jsx new file mode 100644 index 0000000..1757630 --- /dev/null +++ b/src/components/ui/alert-dialog.jsx @@ -0,0 +1,136 @@ +import * as React from "react" +import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog" + +import { cn } from "@/lib/utils" +import { buttonVariants } from "@/components/ui/button" + +function AlertDialog({ + ...props +}) { + return ; +} + +function AlertDialogTrigger({ + ...props +}) { + return (); +} + +function AlertDialogPortal({ + ...props +}) { + return (); +} + +function AlertDialogOverlay({ + className, + ...props +}) { + return ( + + ); +} + +function AlertDialogContent({ + className, + ...props +}) { + return ( + + + + + ); +} + +function AlertDialogHeader({ + className, + ...props +}) { + return ( +
+ ); +} + +function AlertDialogFooter({ + className, + ...props +}) { + return ( +
+ ); +} + +function AlertDialogTitle({ + className, + ...props +}) { + return ( + + ); +} + +function AlertDialogDescription({ + className, + ...props +}) { + return ( + + ); +} + +function AlertDialogAction({ + className, + ...props +}) { + return (); +} + +function AlertDialogCancel({ + className, + ...props +}) { + return ( + + ); +} + +export { + AlertDialog, + AlertDialogPortal, + AlertDialogOverlay, + AlertDialogTrigger, + AlertDialogContent, + AlertDialogHeader, + AlertDialogFooter, + AlertDialogTitle, + AlertDialogDescription, + AlertDialogAction, + AlertDialogCancel, +} diff --git a/src/components/ui/button.jsx b/src/components/ui/button.jsx new file mode 100644 index 0000000..e160b3f --- /dev/null +++ b/src/components/ui/button.jsx @@ -0,0 +1,58 @@ +import * as React from "react" +import { Slot } from "@radix-ui/react-slot" +import { cva } from "class-variance-authority"; + +import { cn } from "@/lib/utils" + +const buttonVariants = cva( + "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive", + { + variants: { + variant: { + default: "bg-primary text-primary-foreground hover:bg-primary/90", + destructive: + "bg-destructive text-white hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60", + outline: + "border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50", + secondary: + "bg-secondary text-secondary-foreground hover:bg-secondary/80", + ghost: + "hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50", + link: "text-primary underline-offset-4 hover:underline", + }, + size: { + default: "h-9 px-4 py-2 has-[>svg]:px-3", + sm: "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5", + lg: "h-10 rounded-md px-6 has-[>svg]:px-4", + icon: "size-9", + "icon-sm": "size-8", + "icon-lg": "size-10", + }, + }, + defaultVariants: { + variant: "default", + size: "default", + }, + } +) + +function Button({ + className, + variant = "default", + size = "default", + asChild = false, + ...props +}) { + const Comp = asChild ? Slot : "button" + + return ( + + ); +} + +export { Button, buttonVariants } diff --git a/src/components/ui/checkbox.jsx b/src/components/ui/checkbox.jsx new file mode 100644 index 0000000..2948885 --- /dev/null +++ b/src/components/ui/checkbox.jsx @@ -0,0 +1,28 @@ +import * as React from "react" +import * as CheckboxPrimitive from "@radix-ui/react-checkbox" +import { CheckIcon } from "lucide-react" + +import { cn } from "@/lib/utils" + +function Checkbox({ + className, + ...props +}) { + return ( + + + + + + ); +} + +export { Checkbox } diff --git a/src/components/ui/popover.jsx b/src/components/ui/popover.jsx new file mode 100644 index 0000000..79cb396 --- /dev/null +++ b/src/components/ui/popover.jsx @@ -0,0 +1,45 @@ +import * as React from "react" +import * as PopoverPrimitive from "@radix-ui/react-popover" + +import { cn } from "@/lib/utils" + +function Popover({ + ...props +}) { + return ; +} + +function PopoverTrigger({ + ...props +}) { + return ; +} + +function PopoverContent({ + className, + align = "center", + sideOffset = 4, + ...props +}) { + return ( + + + + ); +} + +function PopoverAnchor({ + ...props +}) { + return ; +} + +export { Popover, PopoverTrigger, PopoverContent, PopoverAnchor } diff --git a/src/components/ui/sheet.jsx b/src/components/ui/sheet.jsx new file mode 100644 index 0000000..21faa4c --- /dev/null +++ b/src/components/ui/sheet.jsx @@ -0,0 +1,138 @@ +import * as React from "react" +import * as SheetPrimitive from "@radix-ui/react-dialog" +import { XIcon } from "lucide-react" + +import { cn } from "@/lib/utils" + +function Sheet({ + ...props +}) { + return ; +} + +function SheetTrigger({ + ...props +}) { + return ; +} + +function SheetClose({ + ...props +}) { + return ; +} + +function SheetPortal({ + ...props +}) { + return ; +} + +function SheetOverlay({ + className, + ...props +}) { + return ( + + ); +} + +function SheetContent({ + className, + children, + side = "right", + ...props +}) { + return ( + + + + {children} + + + Close + + + + ); +} + +function SheetHeader({ + className, + ...props +}) { + return ( +
+ ); +} + +function SheetFooter({ + className, + ...props +}) { + return ( +
+ ); +} + +function SheetTitle({ + className, + ...props +}) { + return ( + + ); +} + +function SheetDescription({ + className, + ...props +}) { + return ( + + ); +} + +export { + Sheet, + SheetTrigger, + SheetClose, + SheetContent, + SheetHeader, + SheetFooter, + SheetTitle, + SheetDescription, +} diff --git a/src/components/ui/tabs.jsx b/src/components/ui/tabs.jsx index fcc9ce5..acd1afc 100644 --- a/src/components/ui/tabs.jsx +++ b/src/components/ui/tabs.jsx @@ -62,7 +62,7 @@ function TabsTrigger({ className, ...props }) { return ( {/* Section: Warning Table */} - {warning_examples?.length > 0 ?? ( + {warning_rows?.length > 0 ?? (

⚠️ Beberapa nama wilayah perlu diperiksa kembali. @@ -29,7 +29,7 @@ export default function FilePreview({ result }) { 0 ? 5 : 15} + limit={geometry_empty > 0 ? 5 : 10} variant="preview" />