Skip to content

Commit 78a0471

Browse files
Improve import coordinates UI
Add a dedicated Import Coordinates feature with a modal dialog to paste raw coordinates, plus a cleaner PolylineInput UI. Includes a new Import Coordinates button (with modal) and supporting dialog component scaffold to parse and import coordinates into the map. X-Lovable-Edit-ID: edt-8317f505-7481-44ce-ab0e-3aaa9137c2d0
2 parents 77de64d + dbcf62d commit 78a0471

3 files changed

Lines changed: 223 additions & 73 deletions

File tree

Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
import React, { useState } from 'react';
2+
import { Upload, FileText, X } from 'lucide-react';
3+
import {
4+
Dialog,
5+
DialogContent,
6+
DialogHeader,
7+
DialogTitle,
8+
DialogTrigger,
9+
DialogFooter,
10+
} from './ui/dialog';
11+
import { Button } from './ui/button';
12+
13+
interface ImportCoordinatesDialogProps {
14+
onImport: (coordinates: [number, number][]) => void;
15+
}
16+
17+
const ImportCoordinatesDialog: React.FC<ImportCoordinatesDialogProps> = ({ onImport }) => {
18+
const [open, setOpen] = useState(false);
19+
const [text, setText] = useState('');
20+
const [error, setError] = useState('');
21+
22+
const parseCoordinates = (input: string): [number, number][] => {
23+
const lines = input.trim().split(/\n/);
24+
const result: [number, number][] = [];
25+
26+
for (const line of lines) {
27+
const trimmed = line.trim();
28+
if (!trimmed) continue;
29+
30+
const parts = trimmed.split(/[,\s\t]+/).filter(Boolean);
31+
32+
if (parts.length >= 2) {
33+
const first = parseFloat(parts[0]);
34+
const second = parseFloat(parts[1]);
35+
36+
if (!isNaN(first) && !isNaN(second)) {
37+
if (Math.abs(first) <= 180 && Math.abs(second) <= 90) {
38+
result.push([first, second]);
39+
} else if (Math.abs(second) <= 180 && Math.abs(first) <= 90) {
40+
result.push([second, first]);
41+
}
42+
}
43+
}
44+
}
45+
46+
return result;
47+
};
48+
49+
const handleImport = () => {
50+
const parsed = parseCoordinates(text);
51+
if (parsed.length === 0) {
52+
setError('No valid coordinates found. Use format: longitude,latitude (one per line)');
53+
return;
54+
}
55+
onImport(parsed);
56+
setText('');
57+
setError('');
58+
setOpen(false);
59+
};
60+
61+
const previewCount = text ? parseCoordinates(text).length : 0;
62+
63+
return (
64+
<Dialog open={open} onOpenChange={setOpen}>
65+
<DialogTrigger asChild>
66+
<Button variant="outline" size="sm" className="gap-1.5">
67+
<Upload className="h-3.5 w-3.5" />
68+
Import Coordinates
69+
</Button>
70+
</DialogTrigger>
71+
<DialogContent className="sm:max-w-md">
72+
<DialogHeader>
73+
<DialogTitle className="flex items-center gap-2">
74+
<FileText className="h-5 w-5 text-primary" />
75+
Import Coordinates
76+
</DialogTitle>
77+
</DialogHeader>
78+
79+
<div className="space-y-3">
80+
<p className="text-sm text-muted-foreground">
81+
Paste coordinates in any of these formats:
82+
</p>
83+
<div className="rounded-md bg-muted/50 p-3 font-mono text-xs">
84+
<div className="text-muted-foreground">longitude, latitude</div>
85+
<div>-122.4194, 37.7749</div>
86+
<div>-122.4099, 37.7912</div>
87+
</div>
88+
89+
<textarea
90+
value={text}
91+
onChange={e => {
92+
setText(e.target.value);
93+
setError('');
94+
}}
95+
placeholder="Paste your coordinates here..."
96+
className="h-40 w-full resize-none rounded-md border bg-background p-3 font-mono text-sm placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring"
97+
/>
98+
99+
{error && <p className="text-sm text-destructive">{error}</p>}
100+
101+
{text && !error && (
102+
<p className="text-sm text-muted-foreground">
103+
{previewCount > 0
104+
? `Found ${previewCount} valid coordinate${previewCount !== 1 ? 's' : ''}`
105+
: 'No valid coordinates detected'}
106+
</p>
107+
)}
108+
</div>
109+
110+
<DialogFooter className="gap-2 sm:gap-0">
111+
<Button variant="ghost" onClick={() => setOpen(false)}>
112+
Cancel
113+
</Button>
114+
<Button onClick={handleImport} disabled={!text || previewCount === 0}>
115+
Import {previewCount > 0 && `(${previewCount})`}
116+
</Button>
117+
</DialogFooter>
118+
</DialogContent>
119+
</Dialog>
120+
);
121+
};
122+
123+
export default ImportCoordinatesDialog;

src/components/PolylineInput.tsx

Lines changed: 95 additions & 73 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,18 @@
11
import React from 'react';
2-
import { Trash2, Copy, Sparkles } from 'lucide-react';
2+
import { Trash2, Copy, Sparkles, ClipboardPaste } from 'lucide-react';
33
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from './ui/select';
4+
import { Button } from './ui/button';
45
import { decodePolyline } from '../utils/polylineDecoder';
6+
import ImportCoordinatesDialog from './ImportCoordinatesDialog';
7+
import { toast } from 'sonner';
58

69
interface PolylineInputProps {
710
value: string;
811
onChange: (value: string) => void;
912
onClear: () => void;
1013
precision?: number;
1114
onPrecisionChange?: (precision: number) => void;
15+
onCoordinatesImport?: (coordinates: [number, number][]) => void;
1216
}
1317

1418
const PolylineInput: React.FC<PolylineInputProps> = ({
@@ -17,97 +21,115 @@ const PolylineInput: React.FC<PolylineInputProps> = ({
1721
onClear,
1822
precision = 5,
1923
onPrecisionChange,
24+
onCoordinatesImport,
2025
}) => {
2126
const handlePaste = async () => {
2227
try {
2328
const text = await navigator.clipboard.readText();
2429
onChange(text);
30+
toast.success('Polyline pasted');
2531
} catch (err) {
26-
console.error('Failed to read clipboard contents: ', err);
32+
toast.error('Failed to paste from clipboard');
2733
}
2834
};
2935

30-
const primaryCoordinates = value ? decodePolyline(value, precision) : [];
36+
const handleCopy = () => {
37+
navigator.clipboard.writeText(value);
38+
toast.success('Polyline copied');
39+
};
3140

32-
return (
33-
<div className="panel">
34-
<div className="mb-3 flex items-center justify-between">
35-
<span className="rounded-full bg-primary/10 px-2 py-0.5 text-xs font-medium text-primary">
36-
Polyline
37-
</span>
38-
<span className="text-xs text-muted-foreground">
39-
Paste encoded polyline or draw on map
40-
</span>
41-
</div>
41+
const handleImportCoordinates = (coords: [number, number][]) => {
42+
if (onCoordinatesImport) {
43+
onCoordinatesImport(coords);
44+
toast.success(`Imported ${coords.length} coordinates`);
45+
}
46+
};
4247

43-
<div className="mb-2 flex items-center justify-between">
44-
<div />
45-
<div className="flex items-center space-x-1">
46-
{onPrecisionChange && (
47-
<Select
48-
value={precision.toString()}
49-
onValueChange={val => onPrecisionChange(Number(val))}
50-
>
51-
<SelectTrigger className="h-8 min-w-[80px] text-xs">
52-
<SelectValue placeholder="Precision" />
53-
</SelectTrigger>
54-
<SelectContent>
55-
<SelectItem value="5">Precision 5</SelectItem>
56-
<SelectItem value="6">Precision 6</SelectItem>
57-
<SelectItem value="7">Precision 7</SelectItem>
58-
</SelectContent>
59-
</Select>
48+
const loadSample = () => {
49+
onChange(
50+
'}~kvHmzrr@ba\\hnc@jiu@r{Zqx~@hjp@pwEhnc@zhu@zflAbxn@fhjBvqHroaAgcnAp}gAeahAtqGkngAinc@_h|@r{Zad\\y|_D}_y@swg@ysg@}llBpoZqa{@xrw@~eBaaX}{uAero@uqGadY}nr@`dYs_NquNgbjAf{l@|yh@bfc@}nr@z}q@i|i@zgz@r{ZhjFr}gApob@ff}@laIsen@dgYhdPvbIren@'
51+
);
52+
toast.success('Sample polyline loaded');
53+
};
54+
55+
let pointCount = 0;
56+
try {
57+
if (value) {
58+
pointCount = decodePolyline(value, precision).length;
59+
}
60+
} catch {
61+
pointCount = 0;
62+
}
63+
64+
return (
65+
<div className="panel space-y-4">
66+
<div className="flex items-center justify-between">
67+
<div className="flex items-center gap-2">
68+
<h3 className="font-medium">Polyline</h3>
69+
{value && (
70+
<span className="rounded-full bg-primary/10 px-2 py-0.5 text-xs text-primary">
71+
{pointCount} points
72+
</span>
6073
)}
61-
<button
62-
onClick={handlePaste}
63-
className="rounded-md bg-secondary p-1.5 text-xs transition-colors hover:bg-secondary/80"
64-
>
65-
Paste
66-
</button>
67-
<button
68-
onClick={onClear}
69-
className="rounded-md p-1.5 text-muted-foreground transition-colors hover:text-destructive"
70-
disabled={!value}
71-
>
72-
<Trash2 className="h-4 w-4" />
73-
</button>
7474
</div>
75+
{onPrecisionChange && (
76+
<Select
77+
value={precision.toString()}
78+
onValueChange={val => onPrecisionChange(Number(val))}
79+
>
80+
<SelectTrigger className="h-8 w-28 text-xs">
81+
<SelectValue placeholder="Precision" />
82+
</SelectTrigger>
83+
<SelectContent>
84+
<SelectItem value="5">Precision 5</SelectItem>
85+
<SelectItem value="6">Precision 6</SelectItem>
86+
<SelectItem value="7">Precision 7</SelectItem>
87+
</SelectContent>
88+
</Select>
89+
)}
7590
</div>
7691

77-
<textarea
78-
value={value}
79-
onChange={e => onChange(e.target.value)}
80-
placeholder="Paste your encoded polyline here or use edit mode to draw on the map..."
81-
className="h-24 w-full resize-none border-0 bg-transparent p-0 font-mono text-sm placeholder:text-muted-foreground focus:outline-none focus:ring-0"
82-
/>
92+
<div className="relative">
93+
<textarea
94+
value={value}
95+
onChange={e => onChange(e.target.value)}
96+
placeholder="Paste encoded polyline here..."
97+
className="h-28 w-full resize-none rounded-lg border bg-muted/30 p-3 font-mono text-sm placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring"
98+
/>
99+
</div>
83100

84-
<div className="mt-2 flex items-center justify-between">
85-
<div className="text-xs text-muted-foreground">
86-
{value
87-
? `${value.length} characters, ${primaryCoordinates.length} points`
88-
: 'No polyline data'}
101+
<div className="flex flex-wrap items-center justify-between gap-2">
102+
<div className="flex flex-wrap gap-1.5">
103+
<Button variant="outline" size="sm" onClick={handlePaste} className="gap-1.5">
104+
<ClipboardPaste className="h-3.5 w-3.5" />
105+
Paste
106+
</Button>
107+
{onCoordinatesImport && <ImportCoordinatesDialog onImport={handleImportCoordinates} />}
108+
{!value && (
109+
<Button variant="ghost" size="sm" onClick={loadSample} className="gap-1.5 text-primary">
110+
<Sparkles className="h-3.5 w-3.5" />
111+
Sample
112+
</Button>
113+
)}
89114
</div>
90-
<div className="flex space-x-1">
115+
116+
<div className="flex gap-1.5">
91117
{value && (
92-
<button
93-
onClick={() => navigator.clipboard.writeText(value)}
94-
className="flex items-center space-x-1 rounded-md bg-secondary p-1.5 text-xs transition-colors hover:bg-secondary/80"
95-
>
96-
<Copy className="mr-1 h-3 w-3" />
97-
<span>Copy</span>
98-
</button>
118+
<>
119+
<Button variant="secondary" size="sm" onClick={handleCopy} className="gap-1.5">
120+
<Copy className="h-3.5 w-3.5" />
121+
Copy
122+
</Button>
123+
<Button
124+
variant="ghost"
125+
size="sm"
126+
onClick={onClear}
127+
className="text-muted-foreground hover:text-destructive"
128+
>
129+
<Trash2 className="h-3.5 w-3.5" />
130+
</Button>
131+
</>
99132
)}
100-
<button
101-
onClick={() => {
102-
onChange(
103-
'}~kvHmzrr@ba\\hnc@jiu@r{Zqx~@hjp@pwEhnc@zhu@zflAbxn@fhjBvqHroaAgcnAp}gAeahAtqGkngAinc@_h|@r{Zad\\y|_D}_y@swg@ysg@}llBpoZqa{@xrw@~eBaaX}{uAero@uqGadY}nr@`dYs_NquNgbjAf{l@|yh@bfc@}nr@z}q@i|i@zgz@r{ZhjFr}gApob@ff}@laIsen@dgYhdPvbIren@'
104-
);
105-
}}
106-
className="flex items-center space-x-1 rounded-md bg-primary/10 p-1.5 text-xs text-primary transition-colors hover:bg-primary/20"
107-
>
108-
<Sparkles className={`h-3 w-3 ${!value ? 'mr-1' : ''}`} />
109-
{!value && <span>Sample</span>}
110-
</button>
111133
</div>
112134
</div>
113135
</div>

src/pages/Index.tsx

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,10 @@ const Index = () => {
7070
setPrecision,
7171
} = usePolyline(initialHookParams);
7272

73+
const handleCoordinatesImport = (coords: [number, number][]) => {
74+
setCoordinates(coords);
75+
};
76+
7377
const {
7478
comparisonMode,
7579
setComparisonMode,
@@ -287,6 +291,7 @@ const Index = () => {
287291
onClear={handleClear}
288292
precision={precision}
289293
onPrecisionChange={setPrecision}
294+
onCoordinatesImport={handleCoordinatesImport}
290295
/>
291296

292297
<PolylineComparison

0 commit comments

Comments
 (0)