73 lines
2.1 KiB
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>
|