Skip to content

techinmay/csv-merger

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

3 Commits
 
 
 
 

Repository files navigation

<title>CSV Merger Tool</title> <style> *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
:root {
  --bg: #f5f7fa;
  --surface: #ffffff;
  --surface2: #f0f2f5;
  --border: rgba(0,0,0,0.1);
  --border2: rgba(0,0,0,0.18);
  --text: #1a1a1a;
  --text2: #555;
  --text3: #888;
  --teal: #1D9E75;
  --teal-dark: #0F6E56;
  --teal-light: #E1F5EE;
  --amber-bg: #FAEEDA;
  --amber-text: #854F0B;
  --amber-border: #EF9F27;
  --green-bg: #EAF3DE;
  --green-text: #3B6D11;
  --radius-sm: 8px;
  --radius-md: 12px;
  --radius-lg: 16px;
}

@media (prefers-color-scheme: dark) {
  :root {
    --bg: #111;
    --surface: #1c1c1e;
    --surface2: #2a2a2c;
    --border: rgba(255,255,255,0.1);
    --border2: rgba(255,255,255,0.2);
    --text: #f0f0f0;
    --text2: #aaa;
    --text3: #666;
    --teal-light: #042e22;
    --amber-bg: #2e2000;
    --amber-text: #EF9F27;
    --amber-border: #854F0B;
    --green-bg: #172200;
    --green-text: #97C459;
  }
}

body {
  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
  background: var(--bg);
  color: var(--text);
  min-height: 100vh;
  display: flex;
  flex-direction: column;
  align-items: center;
  padding: 2rem 1rem 4rem;
}

header {
  text-align: center;
  margin-bottom: 2rem;
}

header .logo {
  display: inline-flex;
  align-items: center;
  gap: 10px;
  margin-bottom: .75rem;
}

header .logo i {
  font-size: 32px;
  color: var(--teal);
}

header h1 {
  font-size: 26px;
  font-weight: 600;
  color: var(--text);
}

header p {
  font-size: 14px;
  color: var(--text2);
  margin-top: .25rem;
}

.card {
  background: var(--surface);
  border: 0.5px solid var(--border);
  border-radius: var(--radius-lg);
  padding: 1.5rem;
  width: 100%;
  max-width: 640px;
}

/* Drop Zone */
.drop-zone {
  border: 1.5px dashed var(--border2);
  border-radius: var(--radius-md);
  padding: 2.5rem 1.5rem;
  text-align: center;
  cursor: pointer;
  transition: background .15s, border-color .15s;
  background: var(--surface2);
}

.drop-zone.drag-over {
  border-color: var(--teal);
  background: var(--teal-light);
}

.drop-zone .drop-icon {
  font-size: 36px;
  color: var(--teal);
  display: block;
  margin-bottom: .75rem;
}

.drop-zone .drop-title {
  font-size: 15px;
  font-weight: 500;
  margin-bottom: .3rem;
}

.drop-zone .drop-sub {
  font-size: 13px;
  color: var(--text2);
}

.btn-browse {
  display: inline-block;
  margin-top: 1rem;
  padding: 7px 20px;
  border-radius: var(--radius-sm);
  border: 1.5px solid var(--teal);
  color: var(--teal);
  font-size: 13px;
  font-weight: 500;
  cursor: pointer;
  background: transparent;
  transition: background .15s;
}

.btn-browse:hover { background: var(--teal-light); }

/* File List */
.file-list { margin-top: 1rem; display: flex; flex-direction: column; gap: 8px; }

.file-card {
  background: var(--surface);
  border: 0.5px solid var(--border);
  border-radius: var(--radius-sm);
  padding: .7rem 1rem;
  display: flex;
  align-items: center;
  gap: 12px;
}

.file-card .file-icon { font-size: 22px; color: var(--teal); flex-shrink: 0; }

.file-info { flex: 1; min-width: 0; }
.file-name { font-size: 13px; font-weight: 500; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.file-meta { font-size: 11px; color: var(--text3); margin-top: 2px; }

.badge {
  font-size: 11px;
  padding: 2px 9px;
  border-radius: 20px;
  font-weight: 500;
  flex-shrink: 0;
}

.badge-primary { background: var(--teal-light); color: var(--teal-dark); }
.badge-ok { background: var(--green-bg); color: var(--green-text); }

.remove-btn {
  background: none;
  border: none;
  cursor: pointer;
  color: var(--text3);
  font-size: 18px;
  display: flex;
  align-items: center;
  padding: 2px;
  border-radius: 4px;
  flex-shrink: 0;
  transition: color .15s;
}

.remove-btn:hover { color: #E24B4A; }

/* Warning Banner */
.warn-banner {
  margin-top: .75rem;
  background: var(--amber-bg);
  border: 0.5px solid var(--amber-border);
  border-radius: var(--radius-sm);
  padding: .6rem .9rem;
  font-size: 12px;
  color: var(--amber-text);
  display: none;
  align-items: center;
  gap: 8px;
}

/* Options Panel */
.options-panel {
  margin-top: 1rem;
  background: var(--surface2);
  border: 0.5px solid var(--border);
  border-radius: var(--radius-md);
  padding: 1rem 1.25rem;
  display: none;
}

.opt-title {
  font-size: 11px;
  font-weight: 600;
  color: var(--text3);
  text-transform: uppercase;
  letter-spacing: .06em;
  margin-bottom: .85rem;
}

.opt-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 1rem;
  margin-bottom: .65rem;
}

.opt-label { font-size: 13px; }
.opt-label small { display: block; font-size: 11px; color: var(--text3); margin-top: 1px; }

.opt-row select {
  font-size: 13px;
  padding: 5px 10px;
  border-radius: var(--radius-sm);
  border: 0.5px solid var(--border2);
  background: var(--surface);
  color: var(--text);
  cursor: pointer;
  max-width: 200px;
}

.toggle-wrap {
  display: flex;
  align-items: center;
  gap: 7px;
  font-size: 13px;
  cursor: pointer;
}

input[type=checkbox] { width: 15px; height: 15px; accent-color: var(--teal); cursor: pointer; }

/* Merge Button */
.merge-btn {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 8px;
  width: 100%;
  margin-top: 1rem;
  padding: 12px;
  border-radius: var(--radius-sm);
  background: var(--teal);
  color: #fff;
  font-size: 14px;
  font-weight: 500;
  border: none;
  cursor: pointer;
  transition: background .15s;
}

.merge-btn:hover:not(:disabled) { background: var(--teal-dark); }
.merge-btn:disabled { opacity: .5; cursor: not-allowed; }

/* Progress */
.progress-wrap { margin-top: 1rem; display: none; }

.progress-bar-bg {
  height: 6px;
  background: var(--surface2);
  border-radius: 3px;
  overflow: hidden;
  margin-bottom: .5rem;
}

.progress-bar {
  height: 100%;
  background: var(--teal);
  border-radius: 3px;
  width: 0;
  transition: width .3s;
}

.progress-label { font-size: 12px; color: var(--text2); text-align: center; }

/* Summary */
.summary { margin-top: 1rem; display: none; }

.summary-header {
  display: flex;
  align-items: center;
  gap: 8px;
  font-size: 14px;
  font-weight: 500;
  color: var(--teal);
  margin-bottom: 1rem;
}

.summary-header i { font-size: 20px; }

.summary-grid {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 10px;
  margin-bottom: 1rem;
}

.stat-card {
  background: var(--surface2);
  border-radius: var(--radius-sm);
  padding: .85rem;
  text-align: center;
}

.stat-num { font-size: 24px; font-weight: 600; color: var(--teal); }
.stat-lbl { font-size: 11px; color: var(--text3); margin-top: 3px; }

.download-btn {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 8px;
  width: 100%;
  padding: 11px;
  border-radius: var(--radius-sm);
  background: transparent;
  border: 1.5px solid var(--teal);
  color: var(--teal);
  font-size: 13px;
  font-weight: 500;
  cursor: pointer;
  transition: background .15s;
}

.download-btn:hover { background: var(--teal-light); }

.reset-btn {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 6px;
  width: 100%;
  padding: 9px;
  margin-top: 8px;
  border-radius: var(--radius-sm);
  background: transparent;
  border: 0.5px solid var(--border2);
  color: var(--text2);
  font-size: 13px;
  cursor: pointer;
  transition: background .15s;
}

.reset-btn:hover { background: var(--surface2); }

footer {
  margin-top: 2.5rem;
  font-size: 12px;
  color: var(--text3);
  text-align: center;
}
</style>

CSV Merger

Combine multiple CSV files into one — free, private, no upload needed.

<!-- Drop Zone -->
<div class="drop-zone" id="dropZone">
  <i class="ti ti-file-plus drop-icon"></i>
  <div class="drop-title">Drag &amp; drop your CSV files here</div>
  <div class="drop-sub">Supports multiple files at once</div>
  <button class="btn-browse" onclick="document.getElementById('fileInput').click()">Browse files</button>
  <input type="file" id="fileInput" accept=".csv" multiple style="display:none">
</div>

<!-- File List -->
<div class="file-list" id="fileList"></div>

<!-- Warning Banner -->
<div class="warn-banner" id="warnBanner">
  <i class="ti ti-alert-triangle"></i>
  Some files have mismatched column headers. Enable "Allow column mismatch" to merge anyway.
</div>

<!-- Options Panel -->
<div class="options-panel" id="optionsPanel">
  <div class="opt-title">Merge options</div>
  <div class="opt-row">
    <div class="opt-label">
      Primary file
      <small>Its headers become the output template</small>
    </div>
    <select id="primarySelect"></select>
  </div>
  <div class="opt-row" style="margin-top:.25rem">
    <label class="toggle-wrap">
      <input type="checkbox" id="skipDupes"> Skip duplicate rows
    </label>
    <label class="toggle-wrap">
      <input type="checkbox" id="allowMismatch"> Allow column mismatch
    </label>
  </div>
</div>

<!-- Merge Button -->
<button class="merge-btn" id="mergeBtn" disabled>
  <i class="ti ti-git-merge"></i> Merge CSV files
</button>

<!-- Progress -->
<div class="progress-wrap" id="progressWrap">
  <div class="progress-bar-bg"><div class="progress-bar" id="progressBar"></div></div>
  <div class="progress-label" id="progressLabel">Merging...</div>
</div>

<!-- Summary -->
<div class="summary" id="summary">
  <div class="summary-header"><i class="ti ti-circle-check"></i> Merge complete</div>
  <div class="summary-grid">
    <div class="stat-card"><div class="stat-num" id="statFiles">0</div><div class="stat-lbl">Files merged</div></div>
    <div class="stat-card"><div class="stat-num" id="statRows">0</div><div class="stat-lbl">Total rows</div></div>
    <div class="stat-card"><div class="stat-num" id="statCols">0</div><div class="stat-lbl">Columns</div></div>
  </div>
  <button class="download-btn" id="downloadBtn"><i class="ti ti-download"></i> Download merged.csv</button>
  <button class="reset-btn" id="resetBtn"><i class="ti ti-refresh"></i> Merge more files</button>
</div>
All processing happens in your browser — no data is uploaded to any server. <script> const dropZone = document.getElementById('dropZone'); const fileInput = document.getElementById('fileInput'); const fileList = document.getElementById('fileList'); const optionsPanel = document.getElementById('optionsPanel'); const mergeBtn = document.getElementById('mergeBtn'); const progressWrap = document.getElementById('progressWrap'); const progressBar = document.getElementById('progressBar'); const progressLabel = document.getElementById('progressLabel'); const summaryEl = document.getElementById('summary'); const warnBanner = document.getElementById('warnBanner'); const primarySelect = document.getElementById('primarySelect'); let files = []; let mergedBlob = null; dropZone.addEventListener('dragover', e => { e.preventDefault(); dropZone.classList.add('drag-over'); }); dropZone.addEventListener('dragleave', () => dropZone.classList.remove('drag-over')); dropZone.addEventListener('drop', e => { e.preventDefault(); dropZone.classList.remove('drag-over'); addFiles(Array.from(e.dataTransfer.files).filter(f => f.name.endsWith('.csv'))); }); dropZone.addEventListener('click', e => { if (!e.target.classList.contains('btn-browse')) fileInput.click(); }); fileInput.addEventListener('change', () => addFiles(Array.from(fileInput.files))); function fmtSize(b) { if (b < 1024) return b + ' B'; if (b < 1048576) return (b / 1024).toFixed(1) + ' KB'; return (b / 1048576).toFixed(1) + ' MB'; } function addFiles(newFiles) { newFiles.forEach(f => { if (!files.find(x => x.name === f.name && x.size === f.size)) files.push(f); }); renderList(); } function renderList() { fileList.innerHTML = ''; if (files.length === 0) { optionsPanel.style.display = 'none'; mergeBtn.disabled = true; warnBanner.style.display = 'none'; return; } files.forEach((f, i) => { const card = document.createElement('div'); card.className = 'file-card'; card.innerHTML = `
${f.name}
${fmtSize(f.size)}
${i === 0 ? 'primary' : 'added'} `; fileList.appendChild(card); }); fileList.querySelectorAll('.remove-btn').forEach(btn => { btn.addEventListener('click', () => { files.splice(parseInt(btn.dataset.i), 1); renderList(); }); }); if (files.length >= 2) { optionsPanel.style.display = 'block'; primarySelect.innerHTML = files.map((f, i) => `${f.name}`).join(''); mergeBtn.disabled = false; checkHeaders(); } else { optionsPanel.style.display = 'none'; mergeBtn.disabled = true; warnBanner.style.display = 'none'; } } function parseCSV(text) { const lines = text.split(/\r?\n/).filter(l => l.trim()); return lines.map(line => { const cols = []; let cur = ''; let inQ = false; for (let i = 0; i < line.length; i++) { if (line[i] === '"') { inQ = !inQ; } else if (line[i] === ',' && !inQ) { cols.push(cur.trim()); cur = ''; } else cur += line[i]; } cols.push(cur.trim()); return cols; }); } async function readText(file) { return new Promise(res => { const r = new FileReader(); r.onload = e => res(e.target.result); r.readAsText(file); }); } async function checkHeaders() { const headers = await Promise.all(files.map(async f => { const text = await readText(f); return text.split(/\r?\n/)[0] || ''; })); const first = headers[0]; const mismatch = headers.some(h => h !== first); warnBanner.style.display = mismatch ? 'flex' : 'none'; } primarySelect.addEventListener('change', () => { const pi = parseInt(primarySelect.value); const tmp = files[pi]; files.splice(pi, 1); files.unshift(tmp); primarySelect.value = 0; renderList(); }); mergeBtn.addEventListener('click', async () => { progressWrap.style.display = 'block'; progressBar.style.width = '0'; progressLabel.textContent = 'Reading files...'; mergeBtn.disabled = true; summaryEl.style.display = 'none'; const skipDupes = document.getElementById('skipDupes').checked; const allowMismatch = document.getElementById('allowMismatch').checked; const parsed = []; for (let i = 0; i < files.length; i++) { const text = await readText(files[i]); parsed.push(parseCSV(text)); progressBar.style.width = Math.round(((i + 1) / files.length) * 40) + '%'; progressLabel.textContent = `Reading ${files[i].name}...`; } progressBar.style.width = '60%'; progressLabel.textContent = 'Merging data...'; await new Promise(r => setTimeout(r, 80)); const headers = parsed[0][0]; const seenRows = new Set(); let allRows = [headers]; let totalRows = 0; for (let fi = 0; fi < parsed.length; fi++) { const data = parsed[fi]; const fileHeaders = data[0]; const colMap = headers.map(h => fileHeaders.indexOf(h)); for (let ri = 1; ri < data.length; ri++) { const row = data[ri]; if (!row.length || row.every(c => !c)) continue; let outRow; if (allowMismatch || colMap.every(i => i >= 0)) { outRow = colMap.map(ci => ci >= 0 ? (row[ci] || '') : ''); } else { outRow = row; } const key = outRow.join('||'); if (skipDupes && seenRows.has(key)) continue; seenRows.add(key); allRows.push(outRow); totalRows++; } progressBar.style.width = (60 + Math.round(((fi + 1) / parsed.length) * 30)) + '%'; await new Promise(r => setTimeout(r, 30)); } progressBar.style.width = '100%'; progressLabel.textContent = 'Finalising...'; await new Promise(r => setTimeout(r, 120)); const csv = allRows.map(r => r.map(c => (c.includes(',') || c.includes('"') || c.includes('\n')) ? `"${c.replace(/"/g, '""')}"` : c ).join(',') ).join('\n'); mergedBlob = new Blob([csv], { type: 'text/csv' }); progressWrap.style.display = 'none'; summaryEl.style.display = 'block'; document.getElementById('statFiles').textContent = files.length; document.getElementById('statRows').textContent = totalRows.toLocaleString(); document.getElementById('statCols').textContent = headers.length; mergeBtn.disabled = false; }); document.getElementById('downloadBtn').addEventListener('click', () => { if (!mergedBlob) return; const url = URL.createObjectURL(mergedBlob); const a = document.createElement('a'); a.href = url; a.download = 'merged.csv'; a.click(); setTimeout(() => URL.revokeObjectURL(url), 1000); }); document.getElementById('resetBtn').addEventListener('click', () => { files = []; mergedBlob = null; summaryEl.style.display = 'none'; fileList.innerHTML = ''; optionsPanel.style.display = 'none'; mergeBtn.disabled = true; warnBanner.style.display = 'none'; fileInput.value = ''; }); </script>

About

No description, website, or topics provided.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors

Languages