Skip to content

Commit f032b03

Browse files
committed
feat(upload): drop-zone, column-mapper, data-preview, upload-summary components
1 parent 281a07b commit f032b03

4 files changed

Lines changed: 297 additions & 0 deletions

File tree

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
"use client"
2+
3+
import { useState } from "react"
4+
import type { ColumnMapping, UploadSchema } from "@/lib/upload-schemas"
5+
6+
interface ColumnMapperProps {
7+
columns: ColumnMapping[]
8+
schema: UploadSchema | null
9+
onMappingChange: (columns: ColumnMapping[]) => void
10+
}
11+
12+
export function ColumnMapper({ columns, schema, onMappingChange }: ColumnMapperProps) {
13+
const [showAll, setShowAll] = useState(false)
14+
15+
const matched = columns.filter((c) => c.status === "matched")
16+
const unmapped = columns.filter((c) => c.status === "unmapped")
17+
18+
const availableTargets = schema
19+
? schema.columns
20+
.map((c) => c.name)
21+
.filter((name) => !columns.some((col) => col.mappedTo === name))
22+
: []
23+
24+
function handleRemap(header: string, newTarget: string | null) {
25+
const updated = columns.map((col) =>
26+
col.header === header
27+
? { ...col, mappedTo: newTarget, status: (newTarget ? "matched" : "unmapped") as "matched" | "unmapped" }
28+
: col
29+
)
30+
onMappingChange(updated)
31+
}
32+
33+
return (
34+
<div className="border rounded-lg overflow-hidden text-sm">
35+
<div className="grid grid-cols-[1fr_24px_1fr_80px] gap-2 px-4 py-2.5 bg-muted font-semibold border-b">
36+
<span>File Column</span>
37+
<span />
38+
<span>Maps To</span>
39+
<span>Status</span>
40+
</div>
41+
42+
{unmapped.map((col) => (
43+
<div
44+
key={col.header}
45+
className="grid grid-cols-[1fr_24px_1fr_80px] gap-2 px-4 py-2 border-b bg-amber-50/50 items-center"
46+
>
47+
<code className="text-xs bg-muted px-1.5 py-0.5 rounded truncate">
48+
{col.header}
49+
</code>
50+
<span className="text-muted-foreground"></span>
51+
<select
52+
className="text-xs border border-amber-400 rounded px-2 py-1 bg-white"
53+
value={col.mappedTo ?? ""}
54+
onChange={(e) => handleRemap(col.header, e.target.value || null)}
55+
>
56+
<option value="">— select or skip —</option>
57+
{availableTargets.map((t) => (
58+
<option key={t} value={t}>{t}</option>
59+
))}
60+
</select>
61+
<span className="text-xs bg-amber-100 text-amber-700 px-2 py-0.5 rounded text-center">
62+
unmapped
63+
</span>
64+
</div>
65+
))}
66+
67+
{matched.length > 0 && !showAll && (
68+
<button
69+
className="w-full px-4 py-2 text-xs text-muted-foreground hover:bg-muted/50 text-center"
70+
onClick={() => setShowAll(true)}
71+
>
72+
+ {matched.length} matched columns (click to expand)
73+
</button>
74+
)}
75+
76+
{showAll &&
77+
matched.map((col) => (
78+
<div
79+
key={col.header}
80+
className="grid grid-cols-[1fr_24px_1fr_80px] gap-2 px-4 py-2 border-b items-center"
81+
>
82+
<code className="text-xs bg-muted px-1.5 py-0.5 rounded truncate">
83+
{col.header}
84+
</code>
85+
<span className="text-muted-foreground"></span>
86+
<span className="text-xs text-green-700">{col.mappedTo}</span>
87+
<span className="text-xs bg-green-100 text-green-700 px-2 py-0.5 rounded text-center">
88+
matched
89+
</span>
90+
</div>
91+
))}
92+
93+
{showAll && matched.length > 0 && (
94+
<button
95+
className="w-full px-4 py-2 text-xs text-muted-foreground hover:bg-muted/50 text-center"
96+
onClick={() => setShowAll(false)}
97+
>
98+
Collapse matched columns
99+
</button>
100+
)}
101+
</div>
102+
)
103+
}
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
"use client"
2+
3+
interface DataPreviewProps {
4+
headers: string[]
5+
rows: Record<string, string>[]
6+
}
7+
8+
export function DataPreview({ headers, rows }: DataPreviewProps) {
9+
if (rows.length === 0) return null
10+
11+
const displayHeaders = headers.slice(0, 8)
12+
const hasMore = headers.length > 8
13+
14+
return (
15+
<div>
16+
<h3 className="text-sm font-semibold mb-2">Data Preview</h3>
17+
<div className="overflow-x-auto border rounded-lg">
18+
<table className="w-full text-xs border-collapse">
19+
<thead>
20+
<tr className="bg-muted">
21+
{displayHeaders.map((h) => (
22+
<th key={h} className="px-3 py-2 text-left font-semibold border-b whitespace-nowrap">
23+
{h}
24+
</th>
25+
))}
26+
{hasMore && (
27+
<th className="px-3 py-2 text-left font-semibold border-b text-muted-foreground">
28+
+{headers.length - 8} more
29+
</th>
30+
)}
31+
</tr>
32+
</thead>
33+
<tbody>
34+
{rows.map((row, i) => (
35+
<tr key={i} className={i % 2 === 1 ? "bg-muted/30" : ""}>
36+
{displayHeaders.map((h) => (
37+
<td key={h} className="px-3 py-1.5 border-b whitespace-nowrap font-mono max-w-[200px] truncate">
38+
{row[h] ?? ""}
39+
</td>
40+
))}
41+
{hasMore && (
42+
<td className="px-3 py-1.5 border-b text-muted-foreground"></td>
43+
)}
44+
</tr>
45+
))}
46+
</tbody>
47+
</table>
48+
</div>
49+
</div>
50+
)
51+
}
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
"use client"
2+
3+
import { useCallback, useState, useRef } from "react"
4+
import { Upload } from "lucide-react"
5+
import { Button } from "@/components/ui/button"
6+
7+
interface DropZoneProps {
8+
onFile: (file: File) => void
9+
disabled?: boolean
10+
}
11+
12+
export function DropZone({ onFile, disabled }: DropZoneProps) {
13+
const [dragging, setDragging] = useState(false)
14+
const inputRef = useRef<HTMLInputElement>(null)
15+
16+
const handleDrop = useCallback(
17+
(e: React.DragEvent) => {
18+
e.preventDefault()
19+
setDragging(false)
20+
if (disabled) return
21+
const file = e.dataTransfer.files[0]
22+
if (file) onFile(file)
23+
},
24+
[onFile, disabled]
25+
)
26+
27+
const handleDragOver = useCallback((e: React.DragEvent) => {
28+
e.preventDefault()
29+
setDragging(true)
30+
}, [])
31+
32+
const handleDragLeave = useCallback(() => setDragging(false), [])
33+
34+
const handleInputChange = useCallback(
35+
(e: React.ChangeEvent<HTMLInputElement>) => {
36+
const file = e.target.files?.[0]
37+
if (file) onFile(file)
38+
e.target.value = ""
39+
},
40+
[onFile]
41+
)
42+
43+
return (
44+
<div
45+
onDrop={handleDrop}
46+
onDragOver={handleDragOver}
47+
onDragLeave={handleDragLeave}
48+
className={`border-2 border-dashed rounded-xl p-12 text-center transition-colors ${
49+
dragging
50+
? "border-purple-500 bg-purple-50"
51+
: "border-muted-foreground/25 bg-muted/30 hover:border-muted-foreground/40"
52+
} ${disabled ? "opacity-50 pointer-events-none" : "cursor-pointer"}`}
53+
onClick={() => inputRef.current?.click()}
54+
>
55+
<Upload className="mx-auto h-10 w-10 text-muted-foreground mb-3" />
56+
<p className="font-semibold text-sm">Drag & drop your file here</p>
57+
<p className="text-xs text-muted-foreground mt-1 mb-4">
58+
.csv or .xlsx up to 50 MB
59+
</p>
60+
<Button
61+
variant="default"
62+
size="sm"
63+
className="bg-purple-600 hover:bg-purple-700"
64+
onClick={(e) => {
65+
e.stopPropagation()
66+
inputRef.current?.click()
67+
}}
68+
disabled={disabled}
69+
>
70+
Browse Files
71+
</Button>
72+
<input
73+
ref={inputRef}
74+
type="file"
75+
accept=".csv,.xlsx,.xls"
76+
className="hidden"
77+
onChange={handleInputChange}
78+
/>
79+
</div>
80+
)
81+
}
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
"use client"
2+
3+
import { Button } from "@/components/ui/button"
4+
import { CheckCircle } from "lucide-react"
5+
6+
interface UploadSummaryProps {
7+
filename: string
8+
schemaLabel: string
9+
inserted: number
10+
skipped: number
11+
errorCount: number
12+
onUploadAnother: () => void
13+
onViewHistory: () => void
14+
}
15+
16+
export function UploadSummary({
17+
filename,
18+
schemaLabel,
19+
inserted,
20+
skipped,
21+
errorCount,
22+
onUploadAnother,
23+
onViewHistory,
24+
}: UploadSummaryProps) {
25+
return (
26+
<div className="text-center space-y-6">
27+
<div className="bg-green-50 border border-green-200 rounded-xl p-8">
28+
<CheckCircle className="mx-auto h-10 w-10 text-green-600 mb-3" />
29+
<h2 className="text-lg font-bold">Upload Complete</h2>
30+
<p className="text-sm text-muted-foreground mt-1">
31+
{filename}{schemaLabel}
32+
</p>
33+
<div className="flex justify-center gap-8 mt-6 text-sm">
34+
<div>
35+
<span className="text-2xl font-bold text-green-600">{inserted}</span>
36+
<br />
37+
<span className="text-muted-foreground">inserted</span>
38+
</div>
39+
<div>
40+
<span className="text-2xl font-bold text-amber-600">{skipped}</span>
41+
<br />
42+
<span className="text-muted-foreground">skipped</span>
43+
</div>
44+
<div>
45+
<span className="text-2xl font-bold text-red-600">{errorCount}</span>
46+
<br />
47+
<span className="text-muted-foreground">errors</span>
48+
</div>
49+
</div>
50+
</div>
51+
52+
<div className="flex gap-3 justify-center">
53+
<Button className="bg-purple-600 hover:bg-purple-700" onClick={onUploadAnother}>
54+
Upload Another File
55+
</Button>
56+
<Button variant="outline" onClick={onViewHistory}>
57+
View Upload History
58+
</Button>
59+
</div>
60+
</div>
61+
)
62+
}

0 commit comments

Comments
 (0)