From 98aa569e8e644653e1a2081335cce28421275d73 Mon Sep 17 00:00:00 2001 From: Kenneth Brewer Date: Fri, 1 May 2026 04:21:49 -0400 Subject: [PATCH] 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 }