Skip to content

Commit af9348a

Browse files
staredclaude
andcommitted
Enhance CSV upload with drag-and-drop and detailed info display
- Changed "Upload Data" to "Upload CSV" for clarity - Added drag-and-drop functionality with visual feedback (blue highlight) - Display CSV metadata in dropdown format matching Libraries component - Show file dimensions (rows × columns) in button - Added dropdown with file info, row/column counts, and column names - Parse CSV headers to extract column information - Updated CsvData interface to include metadata fields 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <[email protected]>
1 parent 59411f7 commit af9348a

File tree

2 files changed

+249
-16
lines changed

2 files changed

+249
-16
lines changed

src/components/FileUpload.vue

Lines changed: 246 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
<script setup lang="ts">
2-
import { ref } from 'vue'
2+
import { ref, computed, onMounted, onUnmounted } from 'vue'
33
import type { CsvData } from '@/types'
44
55
const emit = defineEmits<{
@@ -9,15 +9,42 @@ const emit = defineEmits<{
99
1010
const fileInputRef = ref<HTMLInputElement>()
1111
const uploadedFile = ref<CsvData | null>(null)
12+
const isOpen = ref(false)
13+
const isDragging = ref(false)
14+
const dropdownRef = ref<HTMLElement>()
15+
16+
const parseCsvInfo = (content: string): { rows: number; columns: number; columnNames: string[] } => {
17+
const lines = content.trim().split('\n')
18+
const columnNames = lines[0].split(',').map(name => name.trim().replace(/^"|"$/g, ''))
19+
return {
20+
rows: lines.length - 1, // Exclude header row
21+
columns: columnNames.length,
22+
columnNames
23+
}
24+
}
1225
1326
const handleDrop = (event: DragEvent) => {
1427
event.preventDefault()
28+
event.stopPropagation()
29+
isDragging.value = false
1530
const files = event.dataTransfer?.files
1631
if (files && files.length > 0) {
1732
processFile(files[0])
1833
}
1934
}
2035
36+
const handleDragOver = (event: DragEvent) => {
37+
event.preventDefault()
38+
event.stopPropagation()
39+
isDragging.value = true
40+
}
41+
42+
const handleDragLeave = (event: DragEvent) => {
43+
event.preventDefault()
44+
event.stopPropagation()
45+
isDragging.value = false
46+
}
47+
2148
const handleFileSelect = (event: Event) => {
2249
const target = event.target as HTMLInputElement
2350
if (target.files && target.files.length > 0) {
@@ -34,9 +61,13 @@ const processFile = (file: File) => {
3461
const reader = new FileReader()
3562
reader.onload = (e) => {
3663
const content = e.target?.result as string
64+
const { rows, columns, columnNames } = parseCsvInfo(content)
3765
const csvData: CsvData = {
3866
name: file.name,
3967
content,
68+
rows,
69+
columns,
70+
columnNames
4071
}
4172
uploadedFile.value = csvData
4273
emit('fileUploaded', csvData)
@@ -46,16 +77,45 @@ const processFile = (file: File) => {
4677
4778
const removeFile = () => {
4879
uploadedFile.value = null
80+
isOpen.value = false
4981
if (fileInputRef.value) {
5082
fileInputRef.value.value = ''
5183
}
5284
emit('fileRemoved')
5385
}
86+
87+
const toggleDropdown = () => {
88+
if (uploadedFile.value) {
89+
isOpen.value = !isOpen.value
90+
}
91+
}
92+
93+
const handleClickOutside = (event: MouseEvent) => {
94+
if (dropdownRef.value && !dropdownRef.value.contains(event.target as Node)) {
95+
isOpen.value = false
96+
}
97+
}
98+
99+
onMounted(() => {
100+
document.addEventListener('click', handleClickOutside)
101+
})
102+
103+
onUnmounted(() => {
104+
document.removeEventListener('click', handleClickOutside)
105+
})
54106
</script>
55107

56108
<template>
57-
<div class="file-upload">
58-
<div v-if="!uploadedFile" class="upload-button" @click="$refs.fileInputRef?.click()">
109+
<div class="file-upload" ref="dropdownRef">
110+
<div
111+
v-if="!uploadedFile"
112+
class="upload-button"
113+
:class="{ 'dragging': isDragging }"
114+
@click="fileInputRef?.click()"
115+
@drop="handleDrop"
116+
@dragover="handleDragOver"
117+
@dragleave="handleDragLeave"
118+
>
59119
<input
60120
ref="fileInputRef"
61121
type="file"
@@ -64,24 +124,70 @@ const removeFile = () => {
64124
class="file-input"
65125
style="display: none;"
66126
/>
67-
📁 Upload Data
127+
<span class="upload-icon">📁</span>
128+
<span class="upload-text">Upload CSV</span>
129+
<span class="drag-hint">or drop file here</span>
68130
</div>
69-
<div v-else class="uploaded-file">
70-
<span class="file-name">📄 {{ uploadedFile.name }}</span>
71-
<button @click="removeFile" class="remove-btn">×</button>
131+
132+
<div v-else class="csv-info-container">
133+
<button
134+
@click="toggleDropdown"
135+
class="csv-button"
136+
>
137+
<span class="csv-icon">📊</span>
138+
<span class="csv-text">{{ uploadedFile.name }} ({{ uploadedFile.rows }} × {{ uploadedFile.columns }})</span>
139+
<span class="dropdown-arrow" :class="{ 'open': isOpen }">▼</span>
140+
</button>
141+
142+
<div v-if="isOpen" class="csv-dropdown">
143+
<div class="csv-header">
144+
<span class="header-text">CSV Information</span>
145+
<button @click="removeFile" class="remove-btn" title="Remove file">×</button>
146+
</div>
147+
<div class="csv-details">
148+
<div class="detail-item">
149+
<span class="detail-label">File:</span>
150+
<span class="detail-value">{{ uploadedFile.name }}</span>
151+
</div>
152+
<div class="detail-item">
153+
<span class="detail-label">Rows:</span>
154+
<span class="detail-value">{{ uploadedFile.rows.toLocaleString() }}</span>
155+
</div>
156+
<div class="detail-item">
157+
<span class="detail-label">Columns:</span>
158+
<span class="detail-value">{{ uploadedFile.columns }}</span>
159+
</div>
160+
<div class="columns-section">
161+
<span class="columns-header">Column Names:</span>
162+
<div class="columns-list">
163+
<span
164+
v-for="(col, index) in uploadedFile.columnNames"
165+
:key="index"
166+
class="column-name"
167+
>
168+
{{ col }}
169+
</span>
170+
</div>
171+
</div>
172+
</div>
173+
</div>
72174
</div>
73175
</div>
74176
</template>
75177

76178
<style scoped>
77179
.file-upload {
180+
position: relative;
78181
display: flex;
79182
align-items: center;
80183
}
81184
82185
.upload-button {
186+
display: flex;
187+
align-items: center;
188+
gap: 0.5rem;
83189
background: #f3f4f6;
84-
border: 1px solid #d1d5db;
190+
border: 2px dashed #d1d5db;
85191
border-radius: 6px;
86192
padding: 0.5rem 0.75rem;
87193
font-size: 0.875rem;
@@ -95,20 +201,93 @@ const removeFile = () => {
95201
border-color: #9ca3af;
96202
}
97203
98-
.uploaded-file {
204+
.upload-button.dragging {
205+
background: #dbeafe;
206+
border-color: #3b82f6;
207+
border-style: solid;
208+
}
209+
210+
.upload-icon {
211+
font-size: 0.875rem;
212+
}
213+
214+
.upload-text {
215+
font-weight: 500;
216+
}
217+
218+
.drag-hint {
219+
font-size: 0.75rem;
220+
color: #6b7280;
221+
margin-left: 0.25rem;
222+
}
223+
224+
.csv-info-container {
225+
position: relative;
226+
}
227+
228+
.csv-button {
99229
display: flex;
100230
align-items: center;
101231
gap: 0.5rem;
102-
background: #ecfdf5;
103-
border: 1px solid #d1fae5;
232+
background: #f3f4f6;
233+
border: 1px solid #d1d5db;
104234
border-radius: 6px;
105235
padding: 0.5rem 0.75rem;
106236
font-size: 0.875rem;
237+
cursor: pointer;
238+
transition: all 0.3s ease;
239+
white-space: nowrap;
240+
}
241+
242+
.csv-button:hover {
243+
background: #e5e7eb;
244+
border-color: #9ca3af;
245+
}
246+
247+
.csv-icon {
248+
font-size: 0.875rem;
107249
}
108250
109-
.file-name {
110-
color: #065f46;
251+
.csv-text {
111252
font-weight: 500;
253+
color: #374151;
254+
}
255+
256+
.dropdown-arrow {
257+
font-size: 0.75rem;
258+
transition: transform 0.3s ease;
259+
margin-left: 0.25rem;
260+
}
261+
262+
.dropdown-arrow.open {
263+
transform: rotate(180deg);
264+
}
265+
266+
.csv-dropdown {
267+
position: absolute;
268+
top: calc(100% + 0.25rem);
269+
right: 0;
270+
background: white;
271+
border: 1px solid #e5e7eb;
272+
border-radius: 6px;
273+
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
274+
z-index: 50;
275+
min-width: 280px;
276+
}
277+
278+
.csv-header {
279+
display: flex;
280+
align-items: center;
281+
justify-content: space-between;
282+
padding: 0.75rem;
283+
border-bottom: 1px solid #e5e7eb;
284+
background: #f9fafb;
285+
}
286+
287+
.header-text {
288+
font-size: 0.875rem;
289+
font-weight: 600;
290+
color: #374151;
112291
}
113292
114293
.remove-btn {
@@ -117,9 +296,9 @@ const removeFile = () => {
117296
color: #6b7280;
118297
cursor: pointer;
119298
padding: 0;
120-
font-size: 1rem;
121-
width: 1.25rem;
122-
height: 1.25rem;
299+
font-size: 1.25rem;
300+
width: 1.5rem;
301+
height: 1.5rem;
123302
display: flex;
124303
align-items: center;
125304
justify-content: center;
@@ -129,6 +308,57 @@ const removeFile = () => {
129308
130309
.remove-btn:hover {
131310
background: #f3f4f6;
311+
color: #ef4444;
312+
}
313+
314+
.csv-details {
315+
padding: 0.75rem;
316+
}
317+
318+
.detail-item {
319+
display: flex;
320+
align-items: center;
321+
gap: 0.5rem;
322+
padding: 0.25rem 0;
323+
font-size: 0.875rem;
324+
}
325+
326+
.detail-label {
327+
font-weight: 500;
328+
color: #6b7280;
329+
min-width: 60px;
330+
}
331+
332+
.detail-value {
333+
color: #374151;
334+
}
335+
336+
.columns-section {
337+
margin-top: 0.75rem;
338+
padding-top: 0.75rem;
339+
border-top: 1px solid #e5e7eb;
340+
}
341+
342+
.columns-header {
343+
font-size: 0.875rem;
344+
font-weight: 500;
345+
color: #6b7280;
346+
display: block;
347+
margin-bottom: 0.5rem;
348+
}
349+
350+
.columns-list {
351+
display: flex;
352+
flex-wrap: wrap;
353+
gap: 0.5rem;
354+
}
355+
356+
.column-name {
357+
background: #f3f4f6;
358+
padding: 0.25rem 0.5rem;
359+
border-radius: 4px;
360+
font-size: 0.75rem;
132361
color: #374151;
362+
font-family: monospace;
133363
}
134364
</style>

src/types/index.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,4 +13,7 @@ export interface WebRMessage {
1313
export interface CsvData {
1414
name: string
1515
content: string
16+
rows: number
17+
columns: number
18+
columnNames: string[]
1619
}

0 commit comments

Comments
 (0)