1
0
Fork 0
polaris-task-force/src/components/frontend/map/AuthoringToolbar.tsx
Z8MB1E fcf220b961 feat(map): /map world map with authoring, placement, and feature APIs
Add the Leaflet map page and client (markers, labels, layer toggles, shipment interpolation), the placement and authoring server actions, the map-features and map-import API routes, and the map styles including leaflet pane isolation so portals stack correctly.

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
2026-09-16 23:14:12 -04:00

778 lines
28 KiB
TypeScript

"use client";
import { useCallback, useEffect, useState } from "react";
import { toast } from "sonner";
import { Button } from "@/components/ui/button";
import {
DropdownMenu,
DropdownMenuCheckboxItem,
DropdownMenuContent,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import {
deleteMapFeature,
saveMapNode,
saveMapRoad,
saveMapZone,
} from "@/app/(frontend)/logistics/map/actions";
import type { MapPoint } from "@/lib/map/points";
import type { NodeFeature, RoadFeature, ZoneFeature } from "@/lib/map/types";
export type AuthoringKind = "road" | "zone" | "node" | null;
export interface EditingFeature {
kind: Exclude<AuthoringKind, null>;
id: number;
label: string;
}
export interface MapAuthoring {
kind: AuthoringKind;
draftPoints: MapPoint[];
canSave: boolean;
saving: boolean;
editing: EditingFeature | null;
fields: {
roadName: string;
setRoadName: (value: string) => void;
roadDescription: string;
setRoadDescription: (value: string) => void;
surface: "paved" | "dirt" | "trail";
setSurface: (value: "paved" | "dirt" | "trail") => void;
speedMultiplier: string;
setSpeedMultiplier: (value: string) => void;
strokeColor: string;
setStrokeColor: (value: string) => void;
strokeWeight: string;
setStrokeWeight: (value: string) => void;
strokeStyle: "solid" | "dashed" | "dotted";
setStrokeStyle: (value: "solid" | "dashed" | "dotted") => void;
labelVisible: boolean;
setLabelVisible: (value: boolean) => void;
zoneName: string;
setZoneName: (value: string) => void;
zoneDescription: string;
setZoneDescription: (value: string) => void;
zoneType: "water" | "land";
setZoneType: (value: "water" | "land") => void;
fillColor: string;
setFillColor: (value: string) => void;
zoneStrokeColor: string;
setZoneStrokeColor: (value: string) => void;
fillOpacity: string;
setFillOpacity: (value: string) => void;
zoneStrokeWeight: string;
setZoneStrokeWeight: (value: string) => void;
nodeName: string;
setNodeName: (value: string) => void;
nodeDescription: string;
setNodeDescription: (value: string) => void;
resourceIds: number[];
toggleResourceId: (id: number) => void;
zoneResourceIds: number[];
toggleZoneResourceId: (id: number) => void;
richness: string;
setRichness: (value: string) => void;
};
setKind: (kind: AuthoringKind) => void;
pushPoint: (point: MapPoint) => void;
clearDraft: () => void;
save: () => Promise<void>;
removeFeature: (target: "road" | "zone" | "node", id: number) => Promise<void>;
startEditingRoad: (road: RoadFeature) => void;
startEditingZone: (zone: ZoneFeature) => void;
startEditingNode: (node: NodeFeature) => void;
}
export function useMapAuthoring(options: { mapId: number; onSaved: () => void }): MapAuthoring {
const { mapId, onSaved } = options;
const [kind, setKindState] = useState<AuthoringKind>(null);
const [draftPoints, setDraftPoints] = useState<MapPoint[]>([]);
const [roadName, setRoadName] = useState("");
const [roadDescription, setRoadDescription] = useState("");
const [surface, setSurface] = useState<"paved" | "dirt" | "trail">("paved");
const [speedMultiplier, setSpeedMultiplier] = useState("1");
const [strokeColor, setStrokeColor] = useState("");
const [strokeWeight, setStrokeWeight] = useState("");
const [strokeStyle, setStrokeStyle] = useState<"solid" | "dashed" | "dotted">("solid");
const [labelVisible, setLabelVisible] = useState(true);
const [zoneName, setZoneName] = useState("");
const [zoneDescription, setZoneDescription] = useState("");
const [zoneType, setZoneType] = useState<"water" | "land">("water");
const [fillColor, setFillColor] = useState("");
const [zoneStrokeColor, setZoneStrokeColor] = useState("");
const [fillOpacity, setFillOpacity] = useState("");
const [zoneStrokeWeight, setZoneStrokeWeight] = useState("");
const [nodeName, setNodeName] = useState("");
const [nodeDescription, setNodeDescription] = useState("");
const [resourceIds, setResourceIds] = useState<number[]>([]);
const [zoneResourceIds, setZoneResourceIds] = useState<number[]>([]);
const [richness, setRichness] = useState("100");
const toggleResourceId = useCallback((id: number) => {
setResourceIds((current) =>
current.includes(id) ? current.filter((entry) => entry !== id) : [...current, id],
);
}, []);
const toggleZoneResourceId = useCallback((id: number) => {
setZoneResourceIds((current) =>
current.includes(id) ? current.filter((entry) => entry !== id) : [...current, id],
);
}, []);
const [saving, setSaving] = useState(false);
const [editing, setEditing] = useState<EditingFeature | null>(null);
const pushPoint = useCallback((point: MapPoint) => {
setDraftPoints((current) => [...current, point]);
}, []);
const clearDraft = useCallback(() => setDraftPoints([]), []);
const setKind = useCallback((next: AuthoringKind) => {
setKindState(next);
setDraftPoints([]);
setEditing(null);
}, []);
const startEditingRoad = useCallback((road: RoadFeature) => {
setKindState("road");
setDraftPoints(road.points.map((point) => [point[0], point[1]]));
setRoadName(road.name ?? "");
setRoadDescription(road.description ?? "");
setSurface(road.surface);
setSpeedMultiplier(String(road.speedMultiplier));
setStrokeColor(road.strokeColor ?? "");
setStrokeWeight(road.strokeWeight != null ? String(road.strokeWeight) : "");
setStrokeStyle(road.strokeStyle ?? "solid");
setLabelVisible(road.labelVisible !== false);
setEditing({ kind: "road", id: road.id, label: road.name ?? `Road #${road.id}` });
}, []);
const startEditingZone = useCallback((zone: ZoneFeature) => {
setKindState("zone");
setDraftPoints(zone.points.map((point) => [point[0], point[1]]));
setZoneName(zone.name ?? "");
setZoneDescription(zone.description ?? "");
setZoneType(zone.type);
setZoneResourceIds(zone.resources.map((entry) => entry.id));
setFillColor(zone.fillColor ?? "");
setZoneStrokeColor(zone.strokeColor ?? "");
setFillOpacity(zone.fillOpacity != null ? String(zone.fillOpacity) : "");
setZoneStrokeWeight(zone.strokeWeight != null ? String(zone.strokeWeight) : "");
setLabelVisible(zone.labelVisible !== false);
setEditing({ kind: "zone", id: zone.id, label: zone.name ?? `Zone #${zone.id}` });
}, []);
const startEditingNode = useCallback((node: NodeFeature) => {
setKindState("node");
setDraftPoints([[node.point[0], node.point[1]]]);
setNodeName(node.name);
setNodeDescription(node.description ?? "");
setResourceIds(node.resources.map((entry) => entry.id));
setRichness(String(node.richness ?? 100));
setEditing({ kind: "node", id: node.id, label: node.name });
}, []);
const removeFeature = useCallback(
async (target: "road" | "zone" | "node", id: number) => {
const result = await deleteMapFeature(target, id);
if (result.success) {
if (editing && editing.id === id && editing.kind === target) setEditing(null);
toast.success("Feature deleted.");
onSaved();
} else {
toast.error(result.error ?? "Delete failed.");
}
},
[editing, onSaved],
);
const save = useCallback(async () => {
if (kind == null) return;
setSaving(true);
try {
if (kind === "road") {
const result = await saveMapRoad({
id: editing?.kind === "road" ? editing.id : undefined,
mapId,
name: roadName.trim() || null,
description: roadDescription.trim() || null,
surface,
speedMultiplier: Number(speedMultiplier) || 1,
strokeColor: strokeColor.trim() || null,
strokeWeight: strokeWeight.trim() ? Number(strokeWeight) : null,
strokeStyle,
labelVisible,
points: draftPoints,
});
if (!result.success) throw new Error(result.error ?? "Save failed.");
toast.success(editing?.kind === "road" ? "Road updated." : "Road saved.");
} else if (kind === "zone") {
const result = await saveMapZone({
id: editing?.kind === "zone" ? editing.id : undefined,
mapId,
name: zoneName.trim() || null,
description: zoneDescription.trim() || null,
type: zoneType,
resourceIds: zoneResourceIds,
fillColor: fillColor.trim() || null,
strokeColor: zoneStrokeColor.trim() || null,
fillOpacity: fillOpacity.trim() ? Number(fillOpacity) : null,
strokeWeight: zoneStrokeWeight.trim() ? Number(zoneStrokeWeight) : null,
labelVisible,
points: draftPoints,
});
if (!result.success) throw new Error(result.error ?? "Save failed.");
toast.success(editing?.kind === "zone" ? "Zone updated." : "Zone saved.");
} else {
const result = await saveMapNode({
id: editing?.kind === "node" ? editing.id : undefined,
mapId,
name: nodeName.trim(),
description: nodeDescription.trim() || null,
resourceIds,
position: draftPoints[0],
richness: Number(richness) || 100,
});
if (!result.success) throw new Error(result.error ?? "Save failed.");
toast.success(editing?.kind === "node" ? "Resource node updated." : "Resource node saved.");
}
setKindState(null);
setDraftPoints([]);
setEditing(null);
setRoadName("");
setRoadDescription("");
setZoneName("");
setZoneDescription("");
setNodeName("");
setNodeDescription("");
setResourceIds([]);
setZoneResourceIds([]);
setStrokeColor("");
setStrokeWeight("");
setStrokeStyle("solid");
setFillColor("");
setZoneStrokeColor("");
setFillOpacity("");
setZoneStrokeWeight("");
setLabelVisible(true);
onSaved();
} catch (error) {
toast.error(error instanceof Error ? error.message : "Save failed.");
} finally {
setSaving(false);
}
}, [
kind,
editing,
mapId,
roadName,
roadDescription,
surface,
speedMultiplier,
strokeColor,
strokeWeight,
strokeStyle,
labelVisible,
draftPoints,
zoneName,
zoneDescription,
zoneType,
fillColor,
zoneStrokeColor,
fillOpacity,
zoneStrokeWeight,
nodeName,
nodeDescription,
resourceIds,
zoneResourceIds,
richness,
onSaved,
]);
const roadReady = kind !== "road" || draftPoints.length >= 2;
const zoneReady = kind !== "zone" || draftPoints.length >= 3;
const nodeReady =
kind !== "node" ||
(draftPoints.length === 1 && resourceIds.length > 0 && nodeName.trim().length > 0);
const canSave = kind != null && roadReady && zoneReady && nodeReady && !saving;
return {
kind,
draftPoints,
canSave,
saving,
editing,
fields: {
roadName,
setRoadName,
roadDescription,
setRoadDescription,
surface,
setSurface,
speedMultiplier,
setSpeedMultiplier,
strokeColor,
setStrokeColor,
strokeWeight,
setStrokeWeight,
strokeStyle,
setStrokeStyle,
labelVisible,
setLabelVisible,
zoneName,
setZoneName,
zoneDescription,
setZoneDescription,
zoneType,
setZoneType,
fillColor,
setFillColor,
zoneStrokeColor,
setZoneStrokeColor,
fillOpacity,
setFillOpacity,
zoneStrokeWeight,
setZoneStrokeWeight,
nodeName,
setNodeName,
nodeDescription,
setNodeDescription,
resourceIds,
toggleResourceId,
zoneResourceIds,
toggleZoneResourceId,
richness,
setRichness,
},
setKind,
pushPoint,
clearDraft,
save,
removeFeature,
startEditingRoad,
startEditingZone,
startEditingNode,
};
}
function useResourceOptions(): { id: number; name: string }[] {
const [options, setOptions] = useState<{ id: number; name: string }[]>([]);
useEffect(() => {
void (async () => {
const response = await fetch("/api/resources?limit=500&depth=0&sort=name", {
cache: "no-store",
});
if (response.ok) {
const body = (await response.json()) as { docs: { id: number; name: string }[] };
setOptions(body.docs);
}
})();
}, []);
return options;
}
function ResourcePicker({
label,
selected,
onToggle,
resources,
}: {
label: string;
selected: number[];
onToggle: (id: number) => void;
resources: { id: number; name: string }[];
}) {
const [query, setQuery] = useState("");
const [open, setOpen] = useState(false);
const needle = query.trim().toLowerCase();
const filtered = needle
? resources.filter((resource) => resource.name.toLowerCase().includes(needle))
: resources;
const summary =
selected.length === 0
? label
: selected.length <= 2
? resources
.filter((resource) => selected.includes(resource.id))
.map((resource) => resource.name)
.join(", ") || label
: `${selected.length} selected`;
return (
<div className="flex flex-col gap-1">
<Label className="text-[10px] uppercase text-white/60">{label}</Label>
<DropdownMenu open={open} onOpenChange={setOpen}>
<DropdownMenuTrigger asChild>
<Button
variant="outline"
className="h-8 w-52 justify-between rounded-none px-2 text-xs font-normal normal-case"
>
<span className="truncate">{summary}</span>
<span className="text-white/40">▾</span>
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent
align="start"
className="z-[1100] w-60 rounded-none border-white/20 bg-black/95"
>
<div className="p-1.5">
<Input
value={query}
onChange={(event) => setQuery(event.target.value)}
placeholder="Search resources..."
className="h-7 rounded-none text-xs"
/>
</div>
<div className="max-h-64 overflow-y-auto">
{filtered.map((resource) => (
<DropdownMenuCheckboxItem
key={resource.id}
checked={selected.includes(resource.id)}
onCheckedChange={() => onToggle(resource.id)}
onSelect={(event) => event.preventDefault()}
className="text-xs"
>
{resource.name}
</DropdownMenuCheckboxItem>
))}
{filtered.length === 0 && (
<div className="px-2 py-3 text-center text-xs text-white/40">No matches</div>
)}
</div>
</DropdownMenuContent>
</DropdownMenu>
</div>
);
}
export function AuthoringToolbar({ authoring, mapId }: { authoring: MapAuthoring; mapId: number }) {
const resources = useResourceOptions();
const { kind, fields } = authoring;
return (
<div className="pointer-events-auto w-[640px] max-w-[calc(100vw-10rem)] overflow-hidden rounded-none border border-white/15 bg-black/80 backdrop-blur-sm">
<div className="flex items-center justify-between gap-6 border-b border-white/15 bg-black/50 px-3 py-1">
<span className="font-mono text-[9px] uppercase tracking-[0.2em] text-white/60">
GM Authoring // Feature Editor
</span>
<span className="font-mono text-[9px] uppercase tracking-[0.2em] text-white/60">
REF {String(mapId).padStart(3, "0")}
</span>
</div>
<div className="flex flex-col gap-2 p-2">
<div className="flex items-center gap-1.5">
{(["road", "zone", "node"] as const).map((target) => (
<Button
key={target}
size="sm"
variant={kind === target && !authoring.editing ? "secondary" : "outline"}
className="h-8 px-2.5 text-xs capitalize"
onClick={() =>
authoring.setKind(kind === target && !authoring.editing ? null : target)
}
>
{target === "node" ? "New node" : `New ${target}`}
</Button>
))}
<span className="ml-auto text-xs text-white/60">
{authoring.draftPoints.length} point{authoring.draftPoints.length === 1 ? "" : "s"}
{kind === "road" && authoring.draftPoints.length < 2 && " (need 2+)"}
{kind === "zone" && authoring.draftPoints.length < 3 && " (need 3+)"}
{kind === "node" && authoring.draftPoints.length === 0 && " (click to set)"}
</span>
<Button
size="sm"
variant="ghost"
className="h-7 px-2 text-xs"
onClick={authoring.clearDraft}
disabled={authoring.draftPoints.length === 0}
>
Clear
</Button>
<Button
size="sm"
className="h-7 px-2 text-xs"
onClick={() => void authoring.save()}
disabled={!authoring.canSave}
>
{authoring.saving ? "Saving..." : authoring.editing ? "Save changes" : "Save"}
</Button>
</div>
{authoring.editing && (
<div className="flex items-center gap-2 rounded-md border border-sky-500/40 bg-sky-950/50 px-2 py-1.5 text-xs text-sky-200">
<span>
Editing {authoring.editing.kind}:{" "}
<span className="font-semibold">{authoring.editing.label}</span>
{kind === "road" && " (click the map to add points to the route)"}
{kind === "zone" && " (click the map to add vertices to the polygon)"}
{kind === "node" && " (click the map to move the node)"}
</span>
<Button
size="sm"
variant="ghost"
className="ml-auto h-6 px-2 text-xs"
onClick={() => authoring.setKind(null)}
>
Cancel edit
</Button>
</div>
)}
{kind === "road" && (
<>
<div className="flex flex-wrap items-end gap-2">
<div className="flex flex-col gap-1">
<Label className="text-[10px] uppercase text-white/60">Name</Label>
<Input
value={fields.roadName}
onChange={(event) => fields.setRoadName(event.target.value)}
placeholder="Road name"
className="h-8 w-40 text-xs"
/>
</div>
<div className="flex flex-col gap-1">
<Label className="text-[10px] uppercase text-white/60">Surface</Label>
<Select
value={fields.surface}
onValueChange={(value) => fields.setSurface(value as "paved" | "dirt" | "trail")}
>
<SelectTrigger className="h-8 w-24 text-xs">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="paved">Paved</SelectItem>
<SelectItem value="dirt">Dirt</SelectItem>
<SelectItem value="trail">Trail</SelectItem>
</SelectContent>
</Select>
</div>
<div className="flex flex-col gap-1">
<Label className="text-[10px] uppercase text-white/60">Speed x</Label>
<Input
type="number"
step="0.1"
min="0.1"
value={fields.speedMultiplier}
onChange={(event) => fields.setSpeedMultiplier(event.target.value)}
className="h-8 w-16 text-xs"
/>
</div>
<div className="flex flex-col gap-1">
<Label className="text-[10px] uppercase text-white/60">Color</Label>
<Input
value={fields.strokeColor}
onChange={(event) => fields.setStrokeColor(event.target.value)}
placeholder="#cbd5e1"
className="h-8 w-24 text-xs"
/>
</div>
<div className="flex flex-col gap-1">
<Label className="text-[10px] uppercase text-white/60">Width</Label>
<Input
type="number"
min="1"
max="10"
value={fields.strokeWeight}
onChange={(event) => fields.setStrokeWeight(event.target.value)}
placeholder="2"
className="h-8 w-14 text-xs"
/>
</div>
<div className="flex flex-col gap-1">
<Label className="text-[10px] uppercase text-white/60">Style</Label>
<Select
value={fields.strokeStyle}
onValueChange={(value) =>
fields.setStrokeStyle(value as "solid" | "dashed" | "dotted")
}
>
<SelectTrigger className="h-8 w-24 text-xs">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="solid">Solid</SelectItem>
<SelectItem value="dashed">Dashed</SelectItem>
<SelectItem value="dotted">Dotted</SelectItem>
</SelectContent>
</Select>
</div>
</div>
<div className="flex items-end gap-3">
<div className="flex flex-1 flex-col gap-1">
<Label className="text-[10px] uppercase text-white/60">Description</Label>
<textarea
value={fields.roadDescription}
onChange={(event) => fields.setRoadDescription(event.target.value)}
placeholder="Shown in the tooltip + popup"
rows={2}
className="w-full rounded-none border border-white/20 bg-black/40 px-2 py-1 text-xs text-white placeholder:text-white/30"
/>
</div>
<label className="flex items-center gap-1.5 pb-1 text-[10px] uppercase tracking-wide text-white/60">
<input
type="checkbox"
checked={fields.labelVisible}
onChange={(event) => fields.setLabelVisible(event.target.checked)}
/>
Label
</label>
</div>
</>
)}
{kind === "zone" && (
<>
<div className="flex flex-wrap items-end gap-2">
<div className="flex flex-col gap-1">
<Label className="text-[10px] uppercase text-white/60">Name</Label>
<Input
value={fields.zoneName}
onChange={(event) => fields.setZoneName(event.target.value)}
placeholder="Zone name"
className="h-8 w-40 text-xs"
/>
</div>
<div className="flex flex-col gap-1">
<Label className="text-[10px] uppercase text-white/60">Type</Label>
<Select
value={fields.zoneType}
onValueChange={(value) => fields.setZoneType(value as "water" | "land")}
>
<SelectTrigger className="h-8 w-24 text-xs">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="water">Water</SelectItem>
<SelectItem value="land">Land</SelectItem>
</SelectContent>
</Select>
</div>
<div className="flex flex-col gap-1">
<Label className="text-[10px] uppercase text-white/60">Fill</Label>
<Input
value={fields.fillColor}
onChange={(event) => fields.setFillColor(event.target.value)}
placeholder="#38bdf8"
className="h-8 w-24 text-xs"
/>
</div>
<div className="flex flex-col gap-1">
<Label className="text-[10px] uppercase text-white/60">Edge</Label>
<Input
value={fields.zoneStrokeColor}
onChange={(event) => fields.setZoneStrokeColor(event.target.value)}
placeholder="#38bdf8"
className="h-8 w-24 text-xs"
/>
</div>
<div className="flex flex-col gap-1">
<Label className="text-[10px] uppercase text-white/60">Opacity</Label>
<Input
type="number"
step="0.05"
min="0"
max="1"
value={fields.fillOpacity}
onChange={(event) => fields.setFillOpacity(event.target.value)}
placeholder="0.25"
className="h-8 w-16 text-xs"
/>
</div>
<div className="flex flex-col gap-1">
<Label className="text-[10px] uppercase text-white/60">Width</Label>
<Input
type="number"
min="0"
max="10"
value={fields.zoneStrokeWeight}
onChange={(event) => fields.setZoneStrokeWeight(event.target.value)}
placeholder="1"
className="h-8 w-14 text-xs"
/>
</div>
<label className="flex items-center gap-1.5 pb-1 text-[10px] uppercase tracking-wide text-white/60">
<input
type="checkbox"
checked={fields.labelVisible}
onChange={(event) => fields.setLabelVisible(event.target.checked)}
/>
Label
</label>
</div>
<ResourcePicker
label="Resources in zone"
selected={fields.zoneResourceIds}
onToggle={fields.toggleZoneResourceId}
resources={resources}
/>
<div className="flex flex-col gap-1">
<Label className="text-[10px] uppercase text-white/60">Description</Label>
<textarea
value={fields.zoneDescription}
onChange={(event) => fields.setZoneDescription(event.target.value)}
placeholder="Shown in the tooltip + popup"
rows={2}
className="w-full rounded-none border border-white/20 bg-black/40 px-2 py-1 text-xs text-white placeholder:text-white/30"
/>
</div>
</>
)}
{kind === "node" && (
<div className="flex flex-wrap items-end gap-2">
<div className="flex flex-col gap-1">
<Label className="text-[10px] uppercase text-white/60">Name</Label>
<Input
value={fields.nodeName}
onChange={(event) => fields.setNodeName(event.target.value)}
placeholder="Node name"
className="h-8 w-40 text-xs"
/>
</div>
<div className="flex flex-col gap-1">
<Label className="text-[10px] uppercase text-white/60">Description</Label>
<textarea
value={fields.nodeDescription}
onChange={(event) => fields.setNodeDescription(event.target.value)}
placeholder="Shown in the tooltip + popup"
rows={2}
className="w-full rounded-none border border-white/20 bg-black/40 px-2 py-1 text-xs text-white placeholder:text-white/30"
/>
</div>
<ResourcePicker
label="Resources"
selected={fields.resourceIds}
onToggle={fields.toggleResourceId}
resources={resources}
/>
<div className="flex flex-col gap-1">
<Label className="text-[10px] uppercase text-white/60">Richness</Label>
<Input
type="number"
min="0"
value={fields.richness}
onChange={(event) => fields.setRichness(event.target.value)}
className="h-8 w-16 text-xs"
/>
</div>
</div>
)}
</div>
</div>
);
}