app.bettersight.io/frontend/src/components/shared/CsvUpload.vue

73 lines
2.1 KiB
Vue

<script setup>
import { ref } from 'vue'
import Papa from 'papaparse'
const emit = defineEmits(['parsed'])
const fileInput = ref(null)
const VALID_MARKETS = [
'GB', 'US', 'AU', 'NZ', 'CA', 'DE', 'FR', 'ES', 'IT',
'NL', 'ZA', 'IN', 'JP', 'SG', 'AE', 'BR', 'MX', 'TT'
]
function isValidUrl(url) {
try {
new URL(url)
return true
} catch {
return false
}
}
function validateRow(row, lineNumber) {
const errors = []
if (!row.name?.trim()) errors.push(`Line ${lineNumber}: name is required`)
if (!isValidUrl(row.website)) errors.push(`Line ${lineNumber}: website must be a valid URL`)
if (!isValidUrl(row.catalogue_url)) errors.push(`Line ${lineNumber}: catalogue_url must be a valid URL`)
if (!VALID_MARKETS.includes(row.primary_market?.toUpperCase())) {
errors.push(`Line ${lineNumber}: primary_market must be a valid ISO country code`)
}
return errors
}
// Parses an uploaded CSV file and validates each row against the
// competitor schema — CLAUDE.md §22, ported near-verbatim.
function parseCompetitorCsv(file) {
return new Promise((resolve, reject) => {
Papa.parse(file, {
header: true,
skipEmptyLines: true,
complete: (results) => {
const valid = []
const invalid = []
results.data.forEach((row, index) => {
if (row.name?.startsWith('#')) return // skip commented example rows
const errors = validateRow(row, index + 2)
if (errors.length === 0) valid.push(row)
else invalid.push({ row, errors })
})
resolve({ valid, invalid })
},
error: (err) => reject(err)
})
})
}
async function onFileChange(event) {
const file = event.target.files?.[0]
if (!file) return
const { valid, invalid } = await parseCompetitorCsv(file)
emit('parsed', { valid, invalid })
fileInput.value.value = ''
}
</script>
<template>
<div>
<input ref="fileInput" type="file" accept=".csv" style="display:none;" @change="onFileChange">
<UButton color="neutral" variant="outline" icon="i-lucide-upload" @click="fileInput.click()">
Upload CSV
</UButton>
</div>
</template>