+
-
- {marker.label}
-
+
+
+ {marker.label}
+
+
);
}
diff --git a/src/app/map/[id]/components/controls/MarkersControl/MarkersList.tsx b/src/app/map/[id]/components/controls/MarkersControl/MarkersList.tsx
index 71200ed1..d409c687 100644
--- a/src/app/map/[id]/components/controls/MarkersControl/MarkersList.tsx
+++ b/src/app/map/[id]/components/controls/MarkersControl/MarkersList.tsx
@@ -40,7 +40,9 @@ import {
} from "@/app/map/[id]/utils/position";
import { useTRPC } from "@/services/trpc/react";
import { LayerType } from "@/types";
+import { useMapConfig } from "../../../hooks/useMapConfig";
import { useMapId } from "../../../hooks/useMapCore";
+import { mapColors } from "../../../styles";
import DataSourceControl from "../DataSourceItem";
import EmptyLayer from "../LayerEmptyMessage";
import MarkerDragOverlay from "./MarkerDragOverlay";
@@ -63,6 +65,7 @@ export default function MarkersList({
const trpc = useTRPC();
const queryClient = useQueryClient();
const { viewConfig } = useMapViews();
+ const { mapConfig, updateMapConfig } = useMapConfig();
const { data: folders = [] } = useFoldersQuery();
const { updateFolder } = useFolderMutations();
const { data: placedMarkers = [] } = usePlacedMarkersQuery();
@@ -277,6 +280,16 @@ export default function MarkersList({
position: newPosition,
});
+ // Update marker color to match folder color
+ const folderColor =
+ mapConfig.folderColors?.[folderId] || mapColors.markers.color;
+ updateMapConfig({
+ placedMarkerColors: {
+ ...mapConfig.placedMarkerColors,
+ [activeMarker.id]: folderColor,
+ },
+ });
+
// Animate movement - pulse the folder that received the marker
setPulsingFolderId(folderId);
} else if (over && over.id === "unassigned") {
@@ -289,6 +302,8 @@ export default function MarkersList({
folderId: null,
position: newPosition,
});
+ // Keep the marker's current color when moved to unassigned
+ // (don't reset it, just keep what it has)
} else if (over && over.id.toString().startsWith("marker-")) {
// Handle reordering within the same container OR moving to a different container
const overMarkerId = over.id.toString().replace("marker-", "");
@@ -325,10 +340,29 @@ export default function MarkersList({
folderId: overMarker.folderId, // Move to the same folder as the marker we're dropping on
position: newPosition,
});
+
+ // Update marker color to match the folder it's being moved to
+ if (overMarker.folderId) {
+ const folderColor =
+ mapConfig.folderColors?.[overMarker.folderId] ||
+ mapColors.markers.color;
+ updateMapConfig({
+ placedMarkerColors: {
+ ...mapConfig.placedMarkerColors,
+ [activeMarker.id]: folderColor,
+ },
+ });
+ }
}
}
},
- [placedMarkers, updatePlacedMarker, setPulsingFolderId],
+ [
+ placedMarkers,
+ updatePlacedMarker,
+ setPulsingFolderId,
+ mapConfig,
+ updateMapConfig,
+ ],
);
const handleDragEndFolder = useCallback(
@@ -397,13 +431,27 @@ export default function MarkersList({
return sortByPositionAndId(folders);
}, [folders]);
- // Get active marker for drag overlay
+ // Get active marker and color for drag overlay
const getActiveMarker = () => {
if (!activeId) return null;
const markerId = activeId.replace("marker-", "");
return placedMarkers.find((marker) => marker.id === markerId) || null;
};
+ const getActiveMarkerColor = () => {
+ const marker = getActiveMarker();
+ if (!marker) return mapColors.markers.color;
+
+ // Get marker color (check folder color first, then marker color, then default)
+ if (marker.folderId && mapConfig.folderColors?.[marker.folderId]) {
+ return mapConfig.folderColors[marker.folderId];
+ }
+ if (mapConfig.placedMarkerColors?.[marker.id]) {
+ return mapConfig.placedMarkerColors[marker.id];
+ }
+ return mapColors.markers.color;
+ };
+
const hasMarkers =
membersDataSource ||
markerDataSources?.length ||
@@ -493,7 +541,10 @@ export default function MarkersList({
{createPortal(
{activeId && getActiveMarker() && (
-
+
)}
,
document.body,
diff --git a/src/app/map/[id]/components/controls/MarkersControl/SortableFolderItem.tsx b/src/app/map/[id]/components/controls/MarkersControl/SortableFolderItem.tsx
index 58ffbe6b..f897a9e2 100644
--- a/src/app/map/[id]/components/controls/MarkersControl/SortableFolderItem.tsx
+++ b/src/app/map/[id]/components/controls/MarkersControl/SortableFolderItem.tsx
@@ -16,18 +16,24 @@ import {
} from "lucide-react";
import { useMemo, useState } from "react";
import { sortByPositionAndId } from "@/app/map/[id]/utils/position";
+import ColorPalette from "@/components/ColorPalette";
import DeleteConfirmationDialog from "@/components/DeleteConfirmationDialog";
import {
ContextMenu,
ContextMenuContent,
ContextMenuItem,
ContextMenuSeparator,
+ ContextMenuSub,
+ ContextMenuSubContent,
+ ContextMenuSubTrigger,
ContextMenuTrigger,
} from "@/shadcn/ui/context-menu";
import { cn } from "@/shadcn/utils";
import { LayerType } from "@/types";
import { useFolderMutations } from "../../../hooks/useFolders";
+import { useMapConfig } from "../../../hooks/useMapConfig";
import { usePlacedMarkerState } from "../../../hooks/usePlacedMarkers";
+import { mapColors } from "../../../styles";
import ControlEditForm from "../ControlEditForm";
import ControlWrapper from "../ControlWrapper";
import SortableMarkerItem from "./SortableMarkerItem";
@@ -78,6 +84,27 @@ export default function SortableFolderItem({
usePlacedMarkerState();
const { updateFolder, deleteFolder } = useFolderMutations();
+ const { mapConfig, updateMapConfig } = useMapConfig();
+
+ // Get current folder color (defaults to marker color)
+ const currentFolderColor =
+ mapConfig.folderColors?.[folder.id] ?? mapColors.markers.color;
+
+ const handleFolderColorChange = (color: string) => {
+ // Update folder color and all marker colors in one operation
+ const updatedMarkerColors = { ...mapConfig.placedMarkerColors };
+ markers.forEach((marker) => {
+ updatedMarkerColors[marker.id] = color;
+ });
+
+ updateMapConfig({
+ folderColors: {
+ ...mapConfig.folderColors,
+ [folder.id]: color,
+ },
+ placedMarkerColors: updatedMarkerColors,
+ });
+ };
const [isExpanded, setExpanded] = useState(false);
const [isEditing, setEditing] = useState(false);
@@ -135,6 +162,7 @@ export default function SortableFolderItem({
layerType={LayerType.Marker}
isVisible={isFolderVisible}
onVisibilityToggle={() => onVisibilityToggle()}
+ color={currentFolderColor}
>
{isEditing ? (
+
+
+
+
+
+
+
+
+
setShowDeleteDialog(true)}
diff --git a/src/app/map/[id]/components/controls/MarkersControl/SortableMarkerItem.tsx b/src/app/map/[id]/components/controls/MarkersControl/SortableMarkerItem.tsx
index 44bfbc7b..bb01ddb2 100644
--- a/src/app/map/[id]/components/controls/MarkersControl/SortableMarkerItem.tsx
+++ b/src/app/map/[id]/components/controls/MarkersControl/SortableMarkerItem.tsx
@@ -5,20 +5,26 @@ import { CSS } from "@dnd-kit/utilities";
import { EyeIcon, EyeOffIcon, PencilIcon, TrashIcon } from "lucide-react";
import { useEffect, useState } from "react";
import { toast } from "sonner";
+import ColorPalette from "@/components/ColorPalette";
import DeleteConfirmationDialog from "@/components/DeleteConfirmationDialog";
import {
ContextMenu,
ContextMenuContent,
ContextMenuItem,
ContextMenuSeparator,
+ ContextMenuSub,
+ ContextMenuSubContent,
+ ContextMenuSubTrigger,
ContextMenuTrigger,
} from "@/shadcn/ui/context-menu";
import { LayerType } from "@/types";
+import { useMapConfig } from "../../../hooks/useMapConfig";
import { useMapRef } from "../../../hooks/useMapCore";
import {
usePlacedMarkerMutations,
usePlacedMarkerState,
} from "../../../hooks/usePlacedMarkers";
+import { mapColors } from "../../../styles";
import ControlEditForm from "../ControlEditForm";
import ControlWrapper from "../ControlWrapper";
import type { PlacedMarker } from "@/server/models/PlacedMarker";
@@ -47,11 +53,25 @@ export default function SortableMarkerItem({
setPlacedMarkerVisibility,
} = usePlacedMarkerState();
const { updatePlacedMarker, deletePlacedMarker } = usePlacedMarkerMutations();
+ const { mapConfig, updateMapConfig } = useMapConfig();
const mapRef = useMapRef();
const [isEditing, setEditing] = useState(false);
const [editText, setEditText] = useState(marker.label);
const [showDeleteDialog, setShowDeleteDialog] = useState(false);
+ // Get current color (defaults to marker color)
+ const currentColor =
+ mapConfig.placedMarkerColors?.[marker.id] ?? mapColors.markers.color;
+
+ const handleColorChange = (color: string) => {
+ updateMapConfig({
+ placedMarkerColors: {
+ ...mapConfig.placedMarkerColors,
+ [marker.id]: color,
+ },
+ });
+ };
+
// Check if this marker is the one being dragged (even outside its container)
const isCurrentlyDragging = isDragging || activeId === `marker-${marker.id}`;
const isVisible = getPlacedMarkerVisibility(marker.id);
@@ -121,6 +141,7 @@ export default function SortableMarkerItem({
onVisibilityToggle={() =>
setPlacedMarkerVisibility(marker.id, !isVisible)
}
+ color={currentColor}
>
{isEditing ? (
+
+
+
+
+
+
+
+
+
setShowDeleteDialog(true)}
diff --git a/src/components/ColorPalette.tsx b/src/components/ColorPalette.tsx
new file mode 100644
index 00000000..39394fbb
--- /dev/null
+++ b/src/components/ColorPalette.tsx
@@ -0,0 +1,58 @@
+"use client";
+
+import { CheckIcon } from "lucide-react";
+import { cn } from "@/shadcn/utils";
+
+export const DEFAULT_COLOR_PALETTE = [
+ "#FF6B6B", // Red
+ "#678DE3", // Blue
+ "#4DAB37", // Green
+ "#FFA500", // Orange
+ "#9B59B6", // Purple
+ "#1ABC9C", // Turquoise
+ "#E67E22", // Carrot/Orange
+ "#34495E", // Dark Blue Grey
+ "#E74C3C", // Dark Red
+ "#3498DB", // Light Blue
+ "#2ECC71", // Emerald
+ "#8E44AD", // Dark Purple
+];
+
+export interface ColorPaletteProps {
+ colors?: string[];
+ selectedColor?: string;
+ onColorSelect: (color: string) => void;
+ className?: string;
+}
+
+export default function ColorPalette({
+ colors = DEFAULT_COLOR_PALETTE,
+ selectedColor,
+ onColorSelect,
+ className,
+}: ColorPaletteProps) {
+ return (
+
+ {colors.map((color) => (
+
+ ))}
+
+ );
+}
diff --git a/src/components/DataSourceItem.tsx b/src/components/DataSourceItem.tsx
index 710d663e..ead47112 100644
--- a/src/components/DataSourceItem.tsx
+++ b/src/components/DataSourceItem.tsx
@@ -1,8 +1,8 @@
import { formatDistanceToNow } from "date-fns";
import { Database, RefreshCw } from "lucide-react";
import DataSourceIcon from "@/components/DataSourceIcon";
-import { DataSourceType } from "@/server/models/DataSource";
import { cn } from "@/shadcn/utils";
+import type { DataSourceType } from "@/server/models/DataSource";
import type { RouterOutputs } from "@/services/trpc/react";
type DataSourceItemType = NonNullable<
@@ -67,15 +67,15 @@ const getDataSourceStyle = (type: DataSourceType | "unknown") => {
const getGeocodingStatus = (dataSource: DataSourceItemType) => {
const geocodingConfig = dataSource.geocodingConfig;
if (geocodingConfig.type === "None") {
- return { status: "No geocoding", color: "text-neutral-500" };
+ return { status: "No geocoding", color: "text-neutral-400" };
}
if (geocodingConfig.type === "Address") {
- return { status: "Address geocoding", color: "text-green-600" };
+ return { status: "Address geocoding", color: "text-neutral-400" };
}
if (geocodingConfig.type === "Code" || geocodingConfig.type === "Name") {
- return { status: "Area-based geocoding", color: "text-blue-600" };
+ return { status: "Area-based geocoding", color: "text-neutral-400" };
}
- return { status: "Geocoding configured", color: "text-blue-600" };
+ return { status: "Geocoding configured", color: "text-neutral-400" };
};
export function DataSourceItem({
@@ -96,62 +96,62 @@ export function DataSourceItem({
return (
-
- {/* Icon */}
-
-
- {/* Content */}
-
- {/* Header */}
-
-
-
{dataSource.name}
-
-
-
- {dataSource.public && (
-
- Public
-
- )}
- {lastImportedText && (
-
-
- {lastImportedText}
-
- )}
-
-
+ {/* Header: Icon and Name */}
+
+
+ {lastImportedText && (
+
+
+ {lastImportedText}
+
+ )}
- {/* Stats */}
-
-
{dataSource.columnDefs.length} columns
-
•
-
{dataSource.recordCount || "Unknown"} records
-
•
-
{geocodingStatus.status}
+
+
+ {dataSource.name}
+
- {/* Type-specific info */}
- {dataSourceType === DataSourceType.ActionNetwork && (
-
- Activist engagement data
-
- )}
- {dataSourceType === DataSourceType.Mailchimp && (
-
Email subscriber data
- )}
- {dataSource.autoImport && (
-
-
- Auto-import enabled
-
- )}
+ {/* Metadata: Consolidated stats and status */}
+
+ {/* Primary stats: Records, columns, and last updated */}
+
+
+ {dataSource.recordCount?.toLocaleString() || "Unknown"} records
+
+ •
+
+ {dataSource.columnDefs.length} columns
+
+
+
+ {/* Secondary info: Geocoding (muted) and status badges */}
+
+
+ {geocodingStatus.status}
+
+
+ {dataSource.public && (
+
+ Public
+
+ )}
+
+ {dataSource.autoImport && (
+
+
+ Auto-import
+
+ )}
+
+
);
}
diff --git a/src/server/models/Map.ts b/src/server/models/Map.ts
index 713eedc6..a5031c4a 100644
--- a/src/server/models/Map.ts
+++ b/src/server/models/Map.ts
@@ -1,9 +1,18 @@
import z from "zod";
import type { ColumnType, Generated, Insertable, Updateable } from "kysely";
+export enum MarkerDisplayMode {
+ Clusters = "clusters",
+ Heatmap = "heatmap",
+}
+
export const mapConfigSchema = z.object({
markerDataSourceIds: z.array(z.string()),
membersDataSourceId: z.string().nullish(),
+ markerDisplayModes: z.record(z.nativeEnum(MarkerDisplayMode)).optional(),
+ markerColors: z.record(z.string()).optional(),
+ placedMarkerColors: z.record(z.string()).optional(),
+ folderColors: z.record(z.string()).optional(),
});
export type MapConfig = z.infer
;
diff --git a/src/server/trpc/routers/map.ts b/src/server/trpc/routers/map.ts
index 1f1480a6..cef5e10b 100644
--- a/src/server/trpc/routers/map.ts
+++ b/src/server/trpc/routers/map.ts
@@ -125,6 +125,10 @@ export const mapRouter = router({
const config = {
markerDataSourceIds: mapConfig.markerDataSourceIds?.filter(Boolean),
membersDataSourceId: mapConfig.membersDataSourceId,
+ markerDisplayModes: mapConfig.markerDisplayModes,
+ markerColors: mapConfig.markerColors,
+ placedMarkerColors: mapConfig.placedMarkerColors,
+ folderColors: mapConfig.folderColors,
} as z.infer;
return updateMap(mapId, { config });