I am working on a feature in my next.js 15 application in which users have to draw the area on the map with different tools and when user finishes drawing it adds the area stay on the map and also it adds a row of some elements in area list on the left side of the page. In the row i have a custom color picker with my own color pallette. I want when user finish drawing it adds the color to the drew area too and when user changes the color of a particular area it will change the color of the area on the map too.
Here is my code for the parent component and here i have that color picker rendering:
"use client";
import type React from "react";
import { useState, useMemo, useRef, useEffect } from "react";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Switch } from "@/components/ui/switch";
import { Label } from "@/components/ui/label";
import {
Check,
MoreVertical,
Pencil,
Eye,
Trash2,
ArrowUpDown,
} from "lucide-react";
import CollectionAreaMap from "./collection-area-map";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog";
import {
Popover,
PopoverContent,
PopoverTrigger,
} from "@/components/ui/popover";
import ColorPicker, { COLOR_PALETTE } from "./color-picker";
// Define the excluded color (grey with pattern)
const EXCLUDED_COLOR = { color: "#808080" };
interface CollectionArea {
id: number;
name: string;
fee: string;
enabled: boolean;
color: string;
featureId?: string; // Store the mapbox feature ID associated with this area
}
type SortDirection = "asc" | "desc" | null;
type SortField = "name" | "fee" | null;
export default function CollectionTerritoriesPage() {
const [isEditMode, setIsEditMode] = useState(true);
const [territoryName, setTerritoryName] = useState(
"Matlock 30 miles Collection Territory"
);
const [territoryDescription, setTerritoryDescription] = useState("");
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
const [areaToDelete, setAreaToDelete] = useState<CollectionArea | null>(null);
const [sortField, setSortField] = useState<SortField>(null);
const [sortDirection, setSortDirection] = useState<SortDirection>(null);
const [focusedAreaId, setFocusedAreaId] = useState<number | null>(null);
// Initialize with an empty collection areas array
const [collectionAreas, setCollectionAreas] = useState<CollectionArea[]>([]);
const nextIdRef = useRef(1);
// Add this ref to store the deleteFeature function from the map component
const deleteFeatureRef = useRef<((featureId: string) => void) | null>(null);
// Sort the collection areas based on the current sort field and direction
const sortedCollectionAreas = useMemo(() => {
if (!sortField || !sortDirection) {
return [...collectionAreas];
}
return [...collectionAreas].sort((a, b) => {
if (sortField === "name") {
return sortDirection === "asc"
? a.name.localeCompare(b.name)
: b.name.localeCompare(a.name);
} else if (sortField === "fee") {
const feeA = Number.parseInt(a.fee) || 0;
const feeB = Number.parseInt(b.fee) || 0;
return sortDirection === "asc" ? feeA - feeB : feeB - feeA;
}
return 0;
});
}, [collectionAreas, sortField, sortDirection]);
// Update nextIdRef whenever areas change
useEffect(() => {
if (collectionAreas.length > 0) {
const maxId = Math.max(...collectionAreas.map((area) => area.id));
nextIdRef.current = maxId + 1;
} else {
nextIdRef.current = 1;
}
}, [collectionAreas]);
const toggleEditMode = () => {
setIsEditMode(!isEditMode);
};
const toggleAreaEnabled = (id: number) => {
setCollectionAreas(
collectionAreas.map((area) =>
area.id === id ? { ...area, enabled: !area.enabled } : area
)
);
};
const handleSort = (field: SortField) => {
if (sortField === field) {
// Toggle direction if same field
setSortDirection(sortDirection === "asc" ? "desc" : "asc");
} else {
// Set new field and default to ascending
setSortField(field);
setSortDirection("asc");
}
};
const updateAreaName = (id: number, name: string) => {
// Check if name is unique
const isDuplicate = collectionAreas.some(
(area) => area.id !== id && area.name === name
);
if (isDuplicate) {
alert("Area name must be unique within the territory");
return;
}
setCollectionAreas(
collectionAreas.map((area) => (area.id === id ? { ...area, name } : area))
);
};
const updateAreaFee = (id: number, fee: string) => {
// Validate fee (3-digit integer)
if (fee !== "" && !/^d{1,3}$/.test(fee)) {
return;
}
setCollectionAreas(
collectionAreas.map((area) => (area.id === id ? { ...area, fee } : area))
);
};
const updateAreaColor = (id: number, color: string) => {
setCollectionAreas(
collectionAreas.map((area) =>
area.id === id ? { ...area, color } : area
)
);
};
const deleteArea = (area: CollectionArea) => {
setAreaToDelete(area);
setDeleteDialogOpen(true);
};
// Updated to delete from map as well
const confirmDeleteArea = () => {
if (areaToDelete) {
// Get the feature ID associated with this area
const featureId = areaToDelete.featureId;
// Remove the area from the state
setCollectionAreas(
collectionAreas.filter((area) => area.id !== areaToDelete.id)
);
// If there's a feature ID and we have a delete function, delete it from the map
if (featureId && deleteFeatureRef.current) {
deleteFeatureRef.current(featureId);
}
setDeleteDialogOpen(false);
setAreaToDelete(null);
}
};
// Handler for when an area is drawn on the map
const handleAreaDrawn = (feature: any) => {
const nextId = nextIdRef.current;
// Get a color from the palette based on the ID
const nextColorIndex = nextId % COLOR_PALETTE.length;
// Create a new area object
const newArea: CollectionArea = {
id: nextId,
name: `Area ${nextId}`,
fee: "",
enabled: true,
color: COLOR_PALETTE[nextColorIndex].color,
featureId: feature.id, // Store the feature ID for reference
};
// Log for debugging
console.log(`Adding new area with ID ${nextId}, featureId: ${feature.id}`);
// Update the state with the new area
setCollectionAreas((prev) => [...prev, newArea]);
// Automatically focus the newly created area
setFocusedAreaId(nextId);
return newArea;
};
const focusArea = (id: number) => {
setFocusedAreaId(id === focusedAreaId ? null : id);
};
return (
<div className="container mx-auto p-4 h-screen flex flex-col">
<div className="grid grid-cols-1 lg:grid-cols-3 gap-4 flex-grow">
{/* Collection Areas Panel */}
<div className="bg-white rounded-lg shadow-sm border p-4 overflow-auto lg:col-span-1">
<div className="flex justify-between items-center mb-4">
<h2 className="text-lg font-semibold">Collection Areas</h2>
{/* "Add Area" button has been removed as requested */}
</div>
{collectionAreas.length === 0 ? (
<div className="text-center py-8 text-gray-500">
{isEditMode ? (
<p>
No areas added yet. Draw an area on the map to add your first
collection area.
</p>
) : (
<p>No collection areas defined.</p>
)}
</div>
) : (
<>
<div className="grid grid-cols-[auto_1fr_auto_auto] gap-2 items-center mb-2 px-2">
<div className="font-medium text-sm text-gray-600"></div>
<div
className="font-medium text-sm text-gray-600 flex items-center cursor-pointer"
onClick={() => handleSort("name")}
>
Name
<ArrowUpDown
className={`ml-1 h-3 w-3 ${
sortField === "name" ? "opacity-100" : "opacity-50"
}`}
/>
</div>
<div
className="font-medium text-sm text-gray-600 text-right flex items-center justify-end cursor-pointer"
onClick={() => handleSort("fee")}
>
Fee
<ArrowUpDown
className={`ml-1 h-3 w-3 ${
sortField === "fee" ? "opacity-100" : "opacity-50"
}`}
/>
</div>
<div className="font-medium text-sm text-gray-600 text-center">
Enable
</div>
<div></div>
</div>
<div className="space-y-2">
{sortedCollectionAreas.map((area) => (
<div
key={area.id}
className={`grid grid-cols-[auto_1fr_auto_auto] gap-2 items-center border-b pb-2 ${
focusedAreaId === area.id ? "bg-gray-100" : ""
}`}
>
<div className="flex items-center">
{isEditMode ? (
<Popover>
<PopoverTrigger asChild>
<div
className="w-4 h-4 rounded-sm cursor-pointer"
style={{
backgroundColor: area.enabled
? area.color
: EXCLUDED_COLOR.color,
}}
></div>
</PopoverTrigger>
<PopoverContent className="p-0 w-[450px]">
<ColorPicker
color={area.color}
onChange={(color) =>
updateAreaColor(area.id, color)
}
disabled={!area.enabled}
/>
</PopoverContent>
</Popover>
) : (
<div
className="w-4 h-4 rounded-sm"
style={{
backgroundColor: area.enabled
? area.color
: EXCLUDED_COLOR.color,
}}
></div>
)}
</div>
<div>
{isEditMode ? (
<Input
value={area.name}
onChange={(e) =>
updateAreaName(area.id, e.target.value)
}
className="h-8 text-sm"
maxLength={20}
disabled={!area.enabled}
/>
) : (
<span>{area.name}</span>
)}
</div>
<div className="text-right">
{isEditMode ? (
area.enabled ? (
<Input
value={area.fee}
onChange={(e) =>
updateAreaFee(area.id, e.target.value)
}
className="h-8 text-sm w-20"
placeholder="0"
/>
) : null
) : area.enabled ? (
`£${area.fee}`
) : (
"Excluded"
)}
</div>
<div className="flex items-center gap-2">
<Switch
checked={area.enabled}
onCheckedChange={() => toggleAreaEnabled(area.id)}
className="data-[state=checked]:bg-blue-500"
disabled={!isEditMode}
/>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
variant="ghost"
size="icon"
className="h-8 w-8"
>
<MoreVertical className="h-4 w-4" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem onClick={() => focusArea(area.id)}>
<Eye className="mr-2 h-4 w-4" />
Focus
</DropdownMenuItem>
{isEditMode && (
<DropdownMenuItem
onClick={() => deleteArea(area)}
className="text-red-600"
>
<Trash2 className="mr-2 h-4 w-4" />
Delete
</DropdownMenuItem>
)}
</DropdownMenuContent>
</DropdownMenu>
</div>
</div>
))}
</div>
</>
)}
{/* Collection Territory Info (only visible in edit mode) */}
{isEditMode && (
<div className="mt-6 border-t pt-4">
<h2 className="text-lg font-semibold mb-4">
Collection Territory Info
</h2>
<div className="space-y-4">
<div className="space-y-2">
<Label htmlFor="territory-name">Name</Label>
<Input
id="territory-name"
value={territoryName}
onChange={(e) => setTerritoryName(e.target.value)}
maxLength={30}
/>
</div>
<div className="space-y-2">
<Label htmlFor="territory-description">Description</Label>
<Input
id="territory-description"
value={territoryDescription}
onChange={(e) => setTerritoryDescription(e.target.value)}
maxLength={200}
/>
</div>
</div>
</div>
)}
</div>
{/* Map Container */}
<div className="lg:col-span-2 h-full" style={{ minHeight: "600px" }}>
<CollectionAreaMap
areas={collectionAreas}
focusedAreaId={focusedAreaId}
onAreaDrawn={handleAreaDrawn}
onAreaDeleted={(deleteFeatureFunction) => {
// Store the delete function from the map component
deleteFeatureRef.current = deleteFeatureFunction;
}}
/>
</div>
</div>
{/* Floating Button */}
<div className="fixed bottom-4 right-4">
<Button
onClick={toggleEditMode}
className="bg-blue-500 hover:bg-blue-600 text-white"
>
{isEditMode ? (
<>
<Check className="mr-2 h-4 w-4" />
Done editing
</>
) : (
<>
<Pencil className="mr-2 h-4 w-4" />
Edit
</>
)}
</Button>
</div>
{/* Delete Confirmation Dialog */}
<Dialog open={deleteDialogOpen} onOpenChange={setDeleteDialogOpen}>
<DialogContent>
<DialogHeader>
<DialogTitle>Delete Area</DialogTitle>
<DialogDescription>
Are you sure you want to delete {areaToDelete?.name}?
</DialogDescription>
</DialogHeader>
<DialogFooter>
<Button
variant="outline"
onClick={() => setDeleteDialogOpen(false)}
>
Cancel
</Button>
<Button variant="destructive" onClick={confirmDeleteArea}>
Delete
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</div>
);
}
I tried different approaches but couldn’t able to resolve this issue in syncing these colors. If anyone knows how to fix that just guide me and i will implement it myself. I can’t add the AreaMap component due to characters limit.