From 98aa569e8e644653e1a2081335cce28421275d73 Mon Sep 17 00:00:00 2001 From: Kenneth Brewer Date: Fri, 1 May 2026 04:21:49 -0400 Subject: [PATCH 1/2] merged in previous branches --- admin/app/models/map_marker.ts | 12 ++ .../1771200000001_create_map_markers_table.ts | 6 +- admin/inertia/components/maps/MarkerPin.tsx | 47 +++--- admin/inertia/hooks/useMapMarkers.ts | 159 ++++++++++-------- admin/inertia/lib/api.ts | 15 +- admin/types/maps.ts | 21 ++- 6 files changed, 151 insertions(+), 109 deletions(-) diff --git a/admin/app/models/map_marker.ts b/admin/app/models/map_marker.ts index 7d588fd..7a53ee9 100644 --- a/admin/app/models/map_marker.ts +++ b/admin/app/models/map_marker.ts @@ -35,6 +35,18 @@ export default class MapMarker extends BaseModel { @column() declare notes: string | null + @column() + declare custom_color: string | null + + @column() + declare icon: string | null + + @column() + declare icon_color: string | null + + @column() + declare visible: boolean + @column.dateTime({ autoCreate: true }) declare created_at: DateTime diff --git a/admin/database/migrations/1771200000001_create_map_markers_table.ts b/admin/database/migrations/1771200000001_create_map_markers_table.ts index 3268de4..ce228d3 100644 --- a/admin/database/migrations/1771200000001_create_map_markers_table.ts +++ b/admin/database/migrations/1771200000001_create_map_markers_table.ts @@ -9,7 +9,11 @@ export default class extends BaseSchema { table.string('name').notNullable() table.double('longitude').notNullable() table.double('latitude').notNullable() - table.string('color', 20).notNullable().defaultTo('orange') + table.string('color', 20).notNullable().defaultTo('orange') // retaining this for backward compatibility + table.string('custom_color', 7).nullable() // "#aabbcc" + table.string('icon', 50).nullable() // "circle", "star", "flag", etc. + table.string('icon_color', 7).nullable() + table.boolean('visible').notNullable().defaultTo(true) table.string('marker_type', 20).notNullable().defaultTo('pin') table.string('route_id').nullable() table.integer('route_order').nullable() diff --git a/admin/inertia/components/maps/MarkerPin.tsx b/admin/inertia/components/maps/MarkerPin.tsx index 44d3e72..8b04343 100644 --- a/admin/inertia/components/maps/MarkerPin.tsx +++ b/admin/inertia/components/maps/MarkerPin.tsx @@ -1,33 +1,43 @@ -import {IconCircleFilled} from '@tabler/icons-react' -import type { ComponentType, CSSProperties } from 'react' +import { IconCircleFilled } from '@tabler/icons-react' +import type { IconProps } from '@tabler/icons-react' +import type { ComponentType } from 'react' -type MarkerIconProps = { - size?: number - color?: string - style?: CSSProperties - className?: string -} +import { PIN_COLORS } from '~/hooks/useMapMarkers' +import type { PinColorId } from '~/hooks/useMapMarkers' interface MarkerPinProps { - color?: string + color?: PinColorId | string | null + customColor?: string | null + icon?: ComponentType + iconColor?: string | null active?: boolean - Icon?: ComponentType - iconColor?: string +} + +const resolvePinColor = (color?: PinColorId | string | null, customColor?: string | null) => { + if (customColor) return customColor + + if (!color) return '#a84a12' + + const preset = PIN_COLORS.find((pinColor) => pinColor.id === color) + return preset?.hex ?? color } export default function MarkerPin({ - color = '#a84a12', - active = false, - Icon = IconCircleFilled, + color = 'orange', + customColor, + icon: Icon = IconCircleFilled, iconColor = '#ffffff', + active = false, }: MarkerPinProps) { + const resolvedColor = resolvePinColor(color, customColor) + const width = active ? 42 : 36 const height = active ? 52 : 46 const iconSize = active ? 18 : 16 return ( ) } - diff --git a/admin/inertia/hooks/useMapMarkers.ts b/admin/inertia/hooks/useMapMarkers.ts index 98e9594..96b3f54 100644 --- a/admin/inertia/hooks/useMapMarkers.ts +++ b/admin/inertia/hooks/useMapMarkers.ts @@ -1,5 +1,7 @@ -import { useState, useCallback, useEffect } from 'react' +import { useCallback, useEffect, useState } from 'react' + import api from '~/lib/api' +import type { MapMarkerResponse } from '../../types/maps' export const PIN_COLORS = [ { id: 'orange', label: 'Orange', hex: '#a84a12' }, @@ -10,7 +12,9 @@ export const PIN_COLORS = [ { id: 'yellow', label: 'Yellow', hex: '#ca8a04' }, ] as const -export type PinColorId = typeof PIN_COLORS[number]['id'] +export type PinColorId = (typeof PIN_COLORS)[number]['id'] + +export type MarkerIcon = 'pin' | 'circle' | 'star' export interface MapMarker { id: number @@ -18,10 +22,52 @@ export interface MapMarker { longitude: number latitude: number color: PinColorId + customColor?: string | null + icon?: MarkerIcon | string | null + iconColor?: string | null + visible: boolean notes?: string | null createdAt: string + updatedAt?: string } +type CreateMapMarkerValues = { + name: string + longitude: number + latitude: number + color?: PinColorId + customColor?: string | null + icon?: MarkerIcon | string | null + iconColor?: string | null + visible?: boolean + notes?: string | null +} + +type UpdateMapMarkerValues = { + name?: string + color?: PinColorId + customColor?: string | null + icon?: MarkerIcon | string | null + iconColor?: string | null + visible?: boolean + notes?: string | null +} + +const mapMarkerResponse = (marker: MapMarkerResponse): MapMarker => ({ + id: marker.id, + name: marker.name, + longitude: marker.longitude, + latitude: marker.latitude, + color: marker.color as PinColorId, + customColor: marker.custom_color ?? null, + icon: marker.icon ?? null, + iconColor: marker.icon_color ?? null, + visible: marker.visible ?? true, + notes: marker.notes ?? null, + createdAt: marker.created_at, + updatedAt: marker.updated_at, +}) + export function useMapMarkers() { const [markers, setMarkers] = useState([]) const [loaded, setLoaded] = useState(false) @@ -29,91 +75,58 @@ export function useMapMarkers() { useEffect(() => { api.listMapMarkers().then((data) => { if (data) { - setMarkers( - data.map((m) => ({ - id: m.id, - name: m.name, - longitude: m.longitude, - latitude: m.latitude, - color: m.color as PinColorId, - notes: m.notes ?? null, - createdAt: m.created_at, - })) - ) + setMarkers(data.map(mapMarkerResponse)) } setLoaded(true) }) }, []) - const addMarker = useCallback( - async ( - name: string, - longitude: number, - latitude: number, - color: PinColorId = 'orange', - notes?: string - ) => { - const result = await api.createMapMarker({ - name, - longitude, - latitude, - color, - notes, - }) + const addMarker = useCallback(async (values: CreateMapMarkerValues) => { + const result = await api.createMapMarker({ + name: values.name, + longitude: values.longitude, + latitude: values.latitude, + color: values.color ?? 'orange', + custom_color: values.customColor ?? null, + icon: values.icon ?? null, + icon_color: values.iconColor ?? null, + visible: values.visible ?? true, + notes: values.notes ?? null, + }) - if (result) { - const marker: MapMarker = { - id: result.id, - name: result.name, - longitude: result.longitude, - latitude: result.latitude, - color: result.color as PinColorId, - notes: result.notes ?? null, - createdAt: result.created_at, - } + if (result) { + const marker = mapMarkerResponse(result) + setMarkers((prev) => [...prev, marker]) + return marker + } - setMarkers((prev) => [...prev, marker]) - return marker - } + return null + }, []) - return null - }, - [] - ) + const updateMarker = useCallback(async (id: number, updates: UpdateMapMarkerValues) => { + const result = await api.updateMapMarker(id, { + name: updates.name, + color: updates.color, + custom_color: updates.customColor, + icon: updates.icon, + icon_color: updates.iconColor, + visible: updates.visible, + notes: updates.notes, + }) - const updateMarker = useCallback( - async ( - id: number, - updates: { - name?: string - color?: string - notes?: string | null - } - ) => { - const result = await api.updateMapMarker(id, updates) + if (result) { + const marker = mapMarkerResponse(result) - if (result) { - setMarkers((prev) => - prev.map((m) => - m.id === id - ? { - ...m, - name: result.name, - color: result.color as PinColorId, - notes: result.notes ?? null, - } - : m - ) - ) - } - }, - [] - ) + setMarkers((prev) => + prev.map((existingMarker) => (existingMarker.id === id ? marker : existingMarker)) + ) + } + }, []) const deleteMarker = useCallback(async (id: number) => { await api.deleteMapMarker(id) - setMarkers((prev) => prev.filter((m) => m.id !== id)) + setMarkers((prev) => prev.filter((marker) => marker.id !== id)) }, []) return { markers, loaded, addMarker, updateMarker, deleteMarker } diff --git a/admin/inertia/lib/api.ts b/admin/inertia/lib/api.ts index e8b995e..5f78cd7 100644 --- a/admin/inertia/lib/api.ts +++ b/admin/inertia/lib/api.ts @@ -10,7 +10,7 @@ import { catchInternal } from './util' import { NomadChatResponse, NomadInstalledModel, NomadOllamaModel, OllamaChatRequest } from '../../types/ollama' import BenchmarkResult from '#models/benchmark_result' import { BenchmarkType, RunBenchmarkResponse, SubmitBenchmarkResponse, UpdateBuilderTagResponse } from '../../types/benchmark' -import type { MapMarkerResponse } from '../../types/maps' +import type {CreateMapMarkerPayload, MapMarkerResponse, UpdateMapMarkerPayload} from '../../types/maps' class API { private client: AxiosInstance @@ -586,23 +586,14 @@ class API { })() } - async createMapMarker(data: { - name: string - notes?: string | null - longitude: number - latitude: number - color?: string - }) { + async createMapMarker(data: CreateMapMarkerPayload) { return catchInternal(async () => { const response = await this.client.post('/maps/markers', data) return response.data })() } - async updateMapMarker( - id: number, - data: { name?: string; notes?: string | null; color?: string } - ) { + async updateMapMarker(id: number, data: UpdateMapMarkerPayload) { return catchInternal(async () => { const response = await this.client.patch(`/maps/markers/${id}`, data) return response.data diff --git a/admin/types/maps.ts b/admin/types/maps.ts index ff3072d..ace3472 100644 --- a/admin/types/maps.ts +++ b/admin/types/maps.ts @@ -22,16 +22,31 @@ export type MapLayer = { [key: string]: any } +export type CreateMapMarkerPayload = { + name: string + notes?: string | null + longitude: number + latitude: number + color?: string + custom_color?: string | null + icon?: string | null + icon_color?: string | null + visible?: boolean +} + +export type UpdateMapMarkerPayload = Partial + export type MapMarkerResponse = { id: number name: string longitude: number latitude: number color: string + custom_color?: string | null + icon?: string | null + icon_color?: string | null + visible?: boolean notes?: string | null - marker_type?: string - route_id?: string | null - route_order?: number | null created_at: string updated_at?: string } From 1af0faf2791e9702c757a7a42ab5fa4ef1c043e4 Mon Sep 17 00:00:00 2001 From: Kenneth Brewer Date: Fri, 1 May 2026 22:13:06 -0400 Subject: [PATCH 2/2] Set up map markers to now be able to toggle the visiblity, provide a custom color, and a custom icon. --- admin/app/controllers/maps_controller.ts | 22 ++ ...sibility_and_icon_fields_to_map_markers.ts | 23 ++ .../components/maps/IconSelectorPopover.tsx | 119 ++++++ .../inertia/components/maps/MapComponent.tsx | 81 ++-- .../components/maps/MapMarkerFormPopup.tsx | 372 +++++++++++------- admin/inertia/components/maps/MarkerPanel.tsx | 164 +++++--- admin/inertia/components/maps/MarkerPin.tsx | 45 ++- 7 files changed, 590 insertions(+), 236 deletions(-) create mode 100644 admin/database/migrations/1771200000002_add_visibility_and_icon_fields_to_map_markers.ts create mode 100644 admin/inertia/components/maps/IconSelectorPopover.tsx diff --git a/admin/app/controllers/maps_controller.ts b/admin/app/controllers/maps_controller.ts index dd93a8b..7503989 100644 --- a/admin/app/controllers/maps_controller.ts +++ b/admin/app/controllers/maps_controller.ts @@ -140,33 +140,49 @@ export default class MapsController { longitude: vine.number().min(-180).max(180), latitude: vine.number().min(-90).max(90), color: vine.string().trim().maxLength(20).optional(), + custom_color: vine.string().trim().maxLength(7).nullable().optional(), + icon: vine.string().trim().maxLength(50).nullable().optional(), + icon_color: vine.string().trim().maxLength(7).nullable().optional(), + visible: vine.boolean().optional(), notes: vine.string().trim().nullable().optional(), marker_type: vine.string().trim().maxLength(20).optional(), }) ) ) + const marker = await MapMarker.create({ name: payload.name, longitude: payload.longitude, latitude: payload.latitude, color: payload.color ?? 'orange', + custom_color: payload.custom_color ?? null, + icon: payload.icon ?? null, + icon_color: payload.icon_color ?? null, + visible: payload.visible ?? true, notes: payload.notes ?? null, marker_type: payload.marker_type ?? 'pin', }) + return marker } async updateMarker({ request, response }: HttpContext) { const { id } = request.params() const marker = await MapMarker.find(id) + if (!marker) { return response.status(404).send({ message: 'Marker not found' }) } + const payload = await request.validateUsing( vine.compile( vine.object({ name: vine.string().trim().minLength(1).maxLength(255).optional(), color: vine.string().trim().maxLength(20).optional(), + custom_color: vine.string().trim().maxLength(7).nullable().optional(), + icon: vine.string().trim().maxLength(50).nullable().optional(), + icon_color: vine.string().trim().maxLength(7).nullable().optional(), + visible: vine.boolean().optional(), longitude: vine.number().min(-180).max(180).optional(), latitude: vine.number().min(-90).max(90).optional(), notes: vine.string().trim().nullable().optional(), @@ -174,12 +190,18 @@ export default class MapsController { }) ) ) + if (payload.name !== undefined) marker.name = payload.name if (payload.color !== undefined) marker.color = payload.color + if (payload.custom_color !== undefined) marker.custom_color = payload.custom_color + if (payload.icon !== undefined) marker.icon = payload.icon + if (payload.icon_color !== undefined) marker.icon_color = payload.icon_color + if (payload.visible !== undefined) marker.visible = payload.visible if (payload.longitude !== undefined) marker.longitude = payload.longitude if (payload.latitude !== undefined) marker.latitude = payload.latitude if (payload.notes !== undefined) marker.notes = payload.notes if (payload.marker_type !== undefined) marker.marker_type = payload.marker_type + await marker.save() return marker } diff --git a/admin/database/migrations/1771200000002_add_visibility_and_icon_fields_to_map_markers.ts b/admin/database/migrations/1771200000002_add_visibility_and_icon_fields_to_map_markers.ts new file mode 100644 index 0000000..3b95e08 --- /dev/null +++ b/admin/database/migrations/1771200000002_add_visibility_and_icon_fields_to_map_markers.ts @@ -0,0 +1,23 @@ +import { BaseSchema } from '@adonisjs/lucid/schema' + +export default class AddVisibilityAndIconFieldsToMapMarkers extends BaseSchema { + protected tableName = 'map_markers' + + async up() { + this.schema.alterTable(this.tableName, (table) => { + table.string('custom_color', 7).nullable() + table.string('icon', 50).nullable() + table.string('icon_color', 7).nullable() + table.boolean('visible').notNullable().defaultTo(true) + }) + } + + async down() { + this.schema.alterTable(this.tableName, (table) => { + table.dropColumn('custom_color') + table.dropColumn('icon') + table.dropColumn('icon_color') + table.dropColumn('visible') + }) + } +} diff --git a/admin/inertia/components/maps/IconSelectorPopover.tsx b/admin/inertia/components/maps/IconSelectorPopover.tsx new file mode 100644 index 0000000..9d47928 --- /dev/null +++ b/admin/inertia/components/maps/IconSelectorPopover.tsx @@ -0,0 +1,119 @@ +import { useMemo, useState } from 'react' +import * as TablerIcons from '@tabler/icons-react' +import type { IconProps } from '@tabler/icons-react' +import type { ComponentType } from 'react' + +const PAGE_SIZE = 48 + +type IconSelectorPopoverProps = { + selectedIcon?: string | null + onSelect: (iconName: string) => void + onClose: () => void +} + +const iconEntries = Object.entries(TablerIcons) + .filter(([name, value]) => { + return ( + name.startsWith('Icon') && + name !== 'Icon' && + value !== null && + (typeof value === 'function' || typeof value === 'object') + ) + }) + .sort(([a], [b]) => a.localeCompare(b)) as Array<[string, ComponentType]> + +export default function IconSelectorPopover({ + selectedIcon, + onSelect, + onClose, + }: IconSelectorPopoverProps) { + const [query, setQuery] = useState('') + const [page, setPage] = useState(0) + + const filteredIcons = useMemo(() => { + const normalizedQuery = query.trim().toLowerCase() + + return iconEntries.filter(([name]) => name.toLowerCase().includes(normalizedQuery)) + }, [query]) + + const pageCount = Math.max(1, Math.ceil(filteredIcons.length / PAGE_SIZE)) + + const pagedIcons = filteredIcons.slice(page * PAGE_SIZE, page * PAGE_SIZE + PAGE_SIZE) + + return ( +
e.stopPropagation()} + onMouseDown={(e) => e.stopPropagation()} + onPointerDown={(e) => e.stopPropagation()} + > + { + setQuery(e.target.value) + setPage(0) + }} + className="mb-2 block w-full rounded border border-border-default bg-surface-primary px-2 py-1 text-sm text-text-primary placeholder:text-text-muted focus:border-desert-green focus:outline-none" + /> + +
+
+ {pagedIcons.map(([name, Icon]) => ( + + ))} +
+
+ +
+ + + + {filteredIcons.length === 0 ? 'No icons' : `${page + 1} / ${pageCount}`} + + + +
+ +
+ +
+
+ ) +} diff --git a/admin/inertia/components/maps/MapComponent.tsx b/admin/inertia/components/maps/MapComponent.tsx index 922713d..0a6c598 100644 --- a/admin/inertia/components/maps/MapComponent.tsx +++ b/admin/inertia/components/maps/MapComponent.tsx @@ -12,7 +12,7 @@ import 'maplibre-gl/dist/maplibre-gl.css' import { Protocol } from 'pmtiles' import { useCallback, useEffect, useRef, useState } from 'react' -import { useMapMarkers, PIN_COLORS } from '~/hooks/useMapMarkers' +import { useMapMarkers } from '~/hooks/useMapMarkers' import MarkerPin from './MarkerPin' import MarkerPanel from './MarkerPanel' @@ -198,6 +198,17 @@ export default function MapComponent({ } }, [mapCommand, confirmDiscardMarkerChanges]) + useEffect(() => { + if (!selectedMarkerId) return + + const marker = markers.find((m) => m.id === selectedMarkerId) + + if (!marker || marker.visible === false) { + setSelectedMarkerId(null) + setEditingMarkerId(null) + } + }, [markers, selectedMarkerId]) + const handleScaleUnitChange = useCallback((unit: ScaleUnit) => { setScaleUnit(unit) localStorage.setItem('nomad:map-scale-unit', unit) @@ -355,38 +366,53 @@ export default function MapComponent({ onMouseEnter={hideCoordinates} /> - {markers.map((marker) => ( - { - e.originalEvent.stopPropagation() + {markers + .filter((marker) => marker.visible) + .map((marker) => ( + { + e.originalEvent.stopPropagation() - if (!confirmDiscardMarkerChanges()) return + if (!confirmDiscardMarkerChanges()) return - setSelectedMarkerId(marker.id === selectedMarkerId ? null : marker.id) - setPlacingMarker(null) - setEditingMarkerId(null) - setHasUnsavedMarkerChanges(false) - setTargetIndicator(null) - }} - > - color.id === marker.color)?.hex} - active={marker.id === selectedMarkerId} - /> - - ))} + setSelectedMarkerId(marker.id === selectedMarkerId ? null : marker.id) + setPlacingMarker(null) + setEditingMarkerId(null) + setHasUnsavedMarkerChanges(false) + setTargetIndicator(null) + }} + > + + + ))} {placingMarker && ( { - await addMarker(name, placingMarker.lng, placingMarker.lat, color, notes || undefined) + onSave={async ({ name, notes, color, customColor, icon }) => { + await addMarker({ + name, + longitude: placingMarker.lng, + latitude: placingMarker.lat, + color, + customColor, + icon, + notes: notes || null, + }) + setPlacingMarker(null) setHasUnsavedMarkerChanges(false) setTargetIndicator(null) @@ -418,13 +444,15 @@ export default function MapComponent({ initialMarker={selectedMarker} onDirtyChange={setHasUnsavedMarkerChanges} onMouseEnter={hideCoordinates} - onSave={async ({ id, name, notes, color }) => { + onSave={async ({ id, name, notes, color, customColor, icon }) => { if (!id) return await updateMarker(id, { name, notes: notes || null, color, + customColor, + icon, }) setEditingMarkerId(null) @@ -448,6 +476,7 @@ export default function MapComponent({ onFlyTo={handleFlyTo} onSelect={setSelectedMarkerId} selectedMarkerId={selectedMarkerId} + onToggleVisibility={(id, visible) => updateMarker(id, { visible })} /> diff --git a/admin/inertia/components/maps/MapMarkerFormPopup.tsx b/admin/inertia/components/maps/MapMarkerFormPopup.tsx index a84278e..4d7ad25 100644 --- a/admin/inertia/components/maps/MapMarkerFormPopup.tsx +++ b/admin/inertia/components/maps/MapMarkerFormPopup.tsx @@ -1,177 +1,251 @@ -import { useCallback, useEffect, useLayoutEffect, useRef, useState } from 'react' -import { Popup } from 'react-map-gl/maplibre' +import {useCallback, useEffect, useLayoutEffect, useRef, useState} from 'react' +import {Popup} from 'react-map-gl/maplibre' +import {IconPalette} from '@tabler/icons-react' -import { PIN_COLORS } from '~/hooks/useMapMarkers' -import type { MapMarker, PinColorId } from '~/hooks/useMapMarkers' +import {IconIcons} from '@tabler/icons-react' +import IconSelectorPopover from './IconSelectorPopover' + +import {PIN_COLORS} from '~/hooks/useMapMarkers' +import type {MapMarker, PinColorId} from '~/hooks/useMapMarkers' const MAX_MARKER_NOTES_LENGTH = 500 const inputClass = - 'block w-full rounded border border-gray-300 bg-transparent px-2 py-1 text-sm text-gray-900 leading-normal placeholder:text-gray-400 focus:outline-none focus:border-gray-500' + 'block w-full rounded border border-gray-300 bg-transparent px-2 py-1 text-sm text-gray-900 leading-normal placeholder:text-gray-400 focus:outline-none focus:border-gray-500' type MapMarkerFormPopupProps = { - longitude: number - latitude: number - initialMarker?: MapMarker - onSave: (values: { - id?: number - name: string - notes: string - color: PinColorId - }) => Promise | void - onCancel: () => void - onDirtyChange?: (dirty: boolean) => void - onMouseEnter?: () => void + longitude: number + latitude: number + initialMarker?: MapMarker + onSave: (values: { + id?: number + name: string + notes: string + color: PinColorId + customColor: string | null + icon: string | null + }) => Promise | void + onCancel: () => void + onDirtyChange?: (dirty: boolean) => void + onMouseEnter?: () => void } export default function MapMarkerFormPopup({ - longitude, - latitude, - initialMarker, - onSave, - onCancel, - onDirtyChange, - onMouseEnter, + longitude, + latitude, + initialMarker, + onSave, + onCancel, + onDirtyChange, + onMouseEnter, }: MapMarkerFormPopupProps) { - const [name, setName] = useState(initialMarker?.name ?? '') - const [notes, setNotes] = useState(initialMarker?.notes ?? '') - const [color, setColor] = useState(initialMarker?.color ?? 'orange') - const [isSaving, setIsSaving] = useState(false) + const [name, setName] = useState(initialMarker?.name ?? '') + const [notes, setNotes] = useState(initialMarker?.notes ?? '') + const [color, setColor] = useState(initialMarker?.color ?? 'orange') + const [customColor, setCustomColor] = useState(initialMarker?.customColor ?? null) + const [isSaving, setIsSaving] = useState(false) - const textareaRef = useRef(null) + const textareaRef = useRef(null) + const nameInputRef = useRef(null) + const colorInputRef = useRef(null) - const resizeTextarea = useCallback(() => { - const textarea = textareaRef.current - if (!textarea) return + const resizeTextarea = useCallback(() => { + const textarea = textareaRef.current + if (!textarea) return - textarea.style.height = 'auto' - textarea.style.height = `${textarea.scrollHeight}px` - }, []) + textarea.style.height = 'auto' + textarea.style.height = `${textarea.scrollHeight}px` + }, []) - useLayoutEffect(() => { - resizeTextarea() - }, [resizeTextarea]) + const [icon, setIcon] = useState(initialMarker?.icon ?? null) + const [showIconSelector, setShowIconSelector] = useState(false) - const nameInputRef = useRef(null) + useLayoutEffect(() => { + resizeTextarea() + }, [resizeTextarea]) - useLayoutEffect(() => { - nameInputRef.current?.focus() - nameInputRef.current?.select() - }, []) + useLayoutEffect(() => { + nameInputRef.current?.focus() + nameInputRef.current?.select() + }, []) - const isDirty = - name !== (initialMarker?.name ?? '') || - notes !== (initialMarker?.notes ?? '') || - color !== (initialMarker?.color ?? 'orange') + const isDirty = + name !== (initialMarker?.name ?? '') || + notes !== (initialMarker?.notes ?? '') || + color !== (initialMarker?.color ?? 'orange') || + customColor !== (initialMarker?.customColor ?? null) || + icon !== (initialMarker?.icon ?? null) - useEffect(() => { - onDirtyChange?.(isDirty) - }, [isDirty, onDirtyChange]) + useEffect(() => { + onDirtyChange?.(isDirty) + }, [isDirty, onDirtyChange]) - const handleSave = async () => { - if (!name.trim() || isSaving) return + const handleSave = async () => { + if (!name.trim() || isSaving) return - try { - setIsSaving(true) + try { + setIsSaving(true) - await onSave({ - id: initialMarker?.id, - name: name.trim(), - notes: notes.trim(), - color, - }) - } finally { - setIsSaving(false) + await onSave({ + id: initialMarker?.id, + name: name.trim(), + notes: notes.trim(), + color, + customColor, + icon, + }) + } finally { + setIsSaving(false) + } } - } - return ( - -
e.stopPropagation()} - onMouseDown={(e) => e.stopPropagation()} - onPointerDown={(e) => e.stopPropagation()} - onMouseEnter={onMouseEnter} - > - setName(e.target.value)} - onKeyDown={(e) => { - if (e.key === 'Enter') handleSave() - if (e.key === 'Escape') onCancel() - }} - className={inputClass} - /> - -