Initial commit: DrinkTracker full-stack app
Next.js 14 drink collection tracker with AI-powered search, menu scanning, ratings, wishlist, sharing, and CSV backup/restore. Features: - Auth (credentials + OAuth ready) - Drink collection with ratings and reviews - AI search via Claude/OpenAI with search history - Menu photo scanning with AI extraction - Wishlist / Try Later system - Public sharing via slug URLs - CSV backup and restore (merge/replace modes) - Docker Compose for Postgres + MinIO + dev server Security: docker-compose files use env var interpolation instead of hardcoded secrets. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
121
src/lib/csv.ts
Normal file
121
src/lib/csv.ts
Normal file
@@ -0,0 +1,121 @@
|
||||
/**
|
||||
* CSV utilities — RFC 4180 compliant, no external dependencies.
|
||||
*/
|
||||
|
||||
/** Escape a field value for CSV (wrap in quotes if needed) */
|
||||
function escapeField(value: string): string {
|
||||
if (
|
||||
value.includes(",") ||
|
||||
value.includes('"') ||
|
||||
value.includes("\n") ||
|
||||
value.includes("\r")
|
||||
) {
|
||||
return `"${value.replace(/"/g, '""')}"`
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
||||
/** Convert an array of objects to a CSV string */
|
||||
export function objectsToCsv(
|
||||
headers: string[],
|
||||
rows: Record<string, string>[]
|
||||
): string {
|
||||
const lines: string[] = []
|
||||
|
||||
// Header row
|
||||
lines.push(headers.map(escapeField).join(","))
|
||||
|
||||
// Data rows
|
||||
for (const row of rows) {
|
||||
const fields = headers.map((h) => escapeField(row[h] ?? ""))
|
||||
lines.push(fields.join(","))
|
||||
}
|
||||
|
||||
return lines.join("\n")
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse a CSV string into an array of objects keyed by header names.
|
||||
* Uses a state-machine parser to correctly handle quoted fields.
|
||||
*/
|
||||
export function csvToObjects(csvText: string): Record<string, string>[] {
|
||||
const rows = parseCsvRows(csvText)
|
||||
if (rows.length < 1) return []
|
||||
|
||||
const headers = rows[0]
|
||||
const result: Record<string, string>[] = []
|
||||
|
||||
for (let i = 1; i < rows.length; i++) {
|
||||
const row = rows[i]
|
||||
// Skip empty rows
|
||||
if (row.length === 1 && row[0] === "") continue
|
||||
|
||||
const obj: Record<string, string> = {}
|
||||
for (let j = 0; j < headers.length; j++) {
|
||||
obj[headers[j]] = row[j] ?? ""
|
||||
}
|
||||
result.push(obj)
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
/** State-machine CSV parser that handles quoted fields correctly */
|
||||
function parseCsvRows(text: string): string[][] {
|
||||
const rows: string[][] = []
|
||||
let currentRow: string[] = []
|
||||
let currentField = ""
|
||||
let inQuotes = false
|
||||
|
||||
for (let i = 0; i < text.length; i++) {
|
||||
const char = text[i]
|
||||
const nextChar = text[i + 1]
|
||||
|
||||
if (inQuotes) {
|
||||
if (char === '"') {
|
||||
if (nextChar === '"') {
|
||||
// Escaped quote
|
||||
currentField += '"'
|
||||
i++ // skip next quote
|
||||
} else {
|
||||
// End of quoted field
|
||||
inQuotes = false
|
||||
}
|
||||
} else {
|
||||
currentField += char
|
||||
}
|
||||
} else {
|
||||
if (char === '"' && currentField === "") {
|
||||
// Start of quoted field
|
||||
inQuotes = true
|
||||
} else if (char === ",") {
|
||||
currentRow.push(currentField)
|
||||
currentField = ""
|
||||
} else if (char === "\n") {
|
||||
currentRow.push(currentField)
|
||||
currentField = ""
|
||||
rows.push(currentRow)
|
||||
currentRow = []
|
||||
} else if (char === "\r") {
|
||||
// Skip carriage return (handle \r\n)
|
||||
if (nextChar === "\n") {
|
||||
i++ // skip \n
|
||||
}
|
||||
currentRow.push(currentField)
|
||||
currentField = ""
|
||||
rows.push(currentRow)
|
||||
currentRow = []
|
||||
} else {
|
||||
currentField += char
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Handle last field/row
|
||||
if (currentField !== "" || currentRow.length > 0) {
|
||||
currentRow.push(currentField)
|
||||
rows.push(currentRow)
|
||||
}
|
||||
|
||||
return rows
|
||||
}
|
||||
Reference in New Issue
Block a user